UNPKG

mcdata-to-json

Version:

Node.js app to turn Minecraft .dat and .json files into unified json.

319 lines (299 loc) 11.8 kB
#!/usr/bin/env node /* eslint-disable no-unused-vars */ const path = require("path"); const fs = require("fs"); const starttime = process.hrtime(); const hrtimefmt = function (timeref) { return `${process.hrtime(timeref)[0]}s, ${(process.hrtime(timeref)[1] / 1000000).toFixed(3)}ms`; }; const Config = require("./lib/Configuration"); const PATHS = require("./lib/helpers/PathReference").paths; const logger = require("./lib/helpers/Logger").getLogger(); const DOMAIN = "Main"; logger.verbose(`Configuration, PathReference, and Logger loaded in ${hrtimefmt(starttime)}`, { domain: DOMAIN }); const libtime = process.hrtime(); const LogsParser = require("./lib/LogsParser"); const ServerDataExtractor = require("./lib/ServerDataTool"); const PlayerData = require("./lib/PlayerData"); const McaParser = require("./lib/McaParser"); const ProfileHelper = require("./lib/ProfileHelper"); const AdvancementsParser = require("./lib/AdvancementsParser"); const { ensureDirSync } = require("./lib/helpers/PathReference"); logger.verbose(`Library imports loaded in ${hrtimefmt(libtime)}`, { domain: DOMAIN }); if (ServerDataExtractor.checkForData()) { fs.promises.writeFile(path.join(PATHS.OUTPUT_DIR, "uuids.json"), JSON.stringify(Config.PLAYERS)).catch((e) => { throw e; }); ServerDataExtractor.convertLevelDat() .then((level) => { logger.info(`Running on Minecraft ${level.Data.Version.Name}`, { domain: DOMAIN }); if (level.Data.Version.Snapshot !== 0) logger.info(`Minecraft snapshot ${level.Data.Version.Snapshot} detected`, { domain: DOMAIN }); if (level.Data.WasModded) logger.info(`Minecraft server was modified from original state`, { domain: DOMAIN }); if (level.Data["Bukkit.Version"]) logger.info(level.Data["Bukkit.Version"], { domain: DOMAIN }); const processPromises = [ LogsParser.parseLogFiles(), PlayerData.convertPlayerdatFiles(), ProfileHelper.updateProfiles(), AdvancementsParser.parseAndSaveAdvancementFiles().then(AdvancementsParser.createServerAdvancementProgress), ]; return Promise.all(processPromises); }) .then((res) => { const combineProms = []; for (const uuid of Object.keys(Config.PLAYERS)) { combineProms.push(combinePlayerData(uuid)); } return Promise.all(combineProms); }) .then((res) => { logger.info(`Compiled player info to ${PATHS.OUTPUT_DIR}`, { domain: DOMAIN }); logger.info( "Starting JSON conversion of the minecraft region files. The first time this is ran it can take a while, especially on large worlds.", { domain: DOMAIN } ); return createJsonForAllRegionDirs(); }) .then((val) => { return buildTileEntityList(path.join(PATHS.CACHED_MCA_JSON_DIR, "world")); }) .then((overworldTEJson) => { const WORLDTE_OUT = path.join(PATHS.OUTPUT_DIR, "world"); ensureDirSync(WORLDTE_OUT); const teWithItems = []; const mobSpawners = []; const signs = []; const lootables = {}; overworldTEJson.map((owtejn) => { Object.keys(owtejn).map((tilentid) => { owtejn[tilentid].map((tileent) => { if (tileent.id === "minecraft:mob_spawner") { mobSpawners.push({ SpawnData: tileent.SpawnData, pos: [tileent.x, tileent.y, tileent.z], }); } else if (tileent.id === "minecraft:sign") { signs.push({ Color: tileent.Color, Text: [ JSON.parse(tileent.Text1.replace(/\\"/, "'")).text, JSON.parse(tileent.Text2.replace(/\\"/, "'")).text, JSON.parse(tileent.Text3.replace(/\\"/, "'")).text, JSON.parse(tileent.Text4.replace(/\\"/, "'")).text, ], pos: [tileent.x, tileent.y, tileent.z], }); } else if (Object.prototype.hasOwnProperty.call(tileent, "Items")) { if (tileent.Items.length > 0) { teWithItems.push({ Items: tileent.Items, id: tileent.id, pos: [tileent.x, tileent.y, tileent.z], }); } } else if (Object.prototype.hasOwnProperty.call(tileent, "LootTable")) { const loottype = tileent.LootTable.split("/")[1]; if (!Object.prototype.hasOwnProperty.call(lootables, loottype)) { lootables[loottype] = []; } lootables[loottype].push({ id: tileent.id, pos: [tileent.x, tileent.y, tileent.z], type: loottype, }); } }); }); }); const writeJsonPromises = []; writeJsonPromises.push( fs.promises.writeFile(path.join(WORLDTE_OUT, "spawners.json"), JSON.stringify(mobSpawners)) ); writeJsonPromises.push( fs.promises.writeFile(path.join(WORLDTE_OUT, "inventories.json"), JSON.stringify(teWithItems)) ); writeJsonPromises.push(fs.promises.writeFile(path.join(WORLDTE_OUT, "loot.json"), JSON.stringify(lootables))); writeJsonPromises.push(fs.promises.writeFile(path.join(WORLDTE_OUT, "signs.json"), JSON.stringify(signs))); writeJsonPromises.push(fs.promises.writeFile(path.join(WORLDTE_OUT, "te.json"), JSON.stringify(overworldTEJson))); return Promise.all(writeJsonPromises); }) .catch((err) => { throw err; }); } else { logger.info(`Missing or imcomplete cache of extracted data from server.jar`, { domain: DOMAIN }); logger.info(`After extraction completes, please re-run mcdata-to-json`, { domain: DOMAIN }); ServerDataExtractor.performExtraction(); } /** * @return {Promise} */ function createJsonForAllRegionDirs() { return new Promise((resolve, reject) => { for (const world in PATHS.WORLD_DIRS) { if (PATHS.WORLD_DIRS[world]) { const possibleMcaDirs = []; const worldpath = PATHS.WORLD_DIRS[world]; if (fs.readdirSync(worldpath).indexOf("region") === -1) { const subdirs = fs .readdirSync(worldpath, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); for (let i = 0; i < subdirs.length; i++) { const dir = subdirs[i]; if (fs.existsSync(path.join(worldpath, dir, "region"))) possibleMcaDirs.push(path.join(worldpath, dir, "region")); } } else { if (fs.existsSync(path.join(worldpath, "region"))) possibleMcaDirs.push(path.join(worldpath, "region")); } logger.verbose(`Possible MCA dirs under ${worldpath}: ${JSON.stringify(possibleMcaDirs)}`, { domain: DOMAIN }); if (possibleMcaDirs.length === 0) { logger.warn(`World Dir ${world} did not have valid MCA sub dir available (or wasn't itself one)`, { domain: DOMAIN, }); } else { McaParser.convertRegionDirToJSON(possibleMcaDirs[0]); } } } resolve(); }); } /** * * @param {string} uuid * @return {Promise} */ function combinePlayerData(uuid) { if (!uuid) throw new Error(`ENOUUID: Not given uuid for combining data.`); const readjsonPromises = []; const STATS_FILE = path.join(PATHS.STATS_DIR, `${uuid}.json`); const ADVANCEMENT_FILE = path.join(PATHS.TEMP_PLAYERDATA_DIR, uuid, "advancements.json"); const PLAYERDATA_FILE = path.join(PATHS.TEMP_PLAYERDATA_DIR, uuid, "playerdata.json"); const PROFILE_FILE = path.join(PATHS.TEMP_PLAYERDATA_DIR, uuid, "profile.json"); const LOG_FILE = path.join(PATHS.TEMP_PLAYERDATA_DIR, uuid, "logs.json"); const PROMISE_FALSE = new Promise((res, rej) => { res(false); }); if (fs.existsSync(STATS_FILE)) readjsonPromises.push(fs.promises.readFile(STATS_FILE)); else { readjsonPromises.push(PROMISE_FALSE); logger.warn(`No parsed stats file exists for ${Config.PLAYERS[uuid]}`, { domain: DOMAIN }); } if (fs.existsSync(ADVANCEMENT_FILE)) readjsonPromises.push(fs.promises.readFile(ADVANCEMENT_FILE)); else { readjsonPromises.push(PROMISE_FALSE); logger.warn(`No parsed advancements file exists for ${Config.PLAYERS[uuid]}`, { domain: DOMAIN }); } if (fs.existsSync(PLAYERDATA_FILE)) readjsonPromises.push(fs.promises.readFile(PLAYERDATA_FILE)); else { readjsonPromises.push(PROMISE_FALSE); logger.warn(`No parsed playerdata file exists for ${Config.PLAYERS[uuid]}`, { domain: DOMAIN }); } if (fs.existsSync(PROFILE_FILE)) readjsonPromises.push(fs.promises.readFile(PROFILE_FILE)); else { readjsonPromises.push(PROMISE_FALSE); logger.warn(`No parsed profile file exists for ${Config.PLAYERS[uuid]}`, { domain: DOMAIN }); } if (fs.existsSync(LOG_FILE)) readjsonPromises.push(fs.promises.readFile(LOG_FILE)); else { readjsonPromises.push(PROMISE_FALSE); logger.warn(`No parsed log file exists for ${Config.PLAYERS[uuid]}`, { domain: DOMAIN }); } return Promise.all(readjsonPromises) .then((val) => { const playerJSON = { advancements: {}, data: {}, log: {}, name: Config.PLAYERS[uuid], profile: {}, stats: {}, uuid: uuid, }; if (val[0]) { playerJSON.stats = JSON.parse(val[0]); } if (val[1]) { playerJSON.advancements = JSON.parse(val[1]); } if (val[2]) { playerJSON.data = JSON.parse(val[2]); } if (val[3]) { playerJSON.profile = JSON.parse(val[3]); } if (val[4]) { playerJSON.log = JSON.parse(val[4]); } delete playerJSON.advancements.DataVersion; delete playerJSON.data.DataVersion; delete playerJSON.stats.DataVersion; return fs.promises.writeFile(path.join(PATHS.OUTPUT_DIR, `${uuid}.json`), JSON.stringify(playerJSON)); }) .then((val) => { logger.verbose(`Wrote output JSON for ${Config.PLAYERS[uuid]} (${uuid})`, { domain: DOMAIN }); if (val) { logger.debug(val, DOMAIN); } }) .catch((err) => { logger.warn(`Failed to build output for ${uuid}.`, { domain: DOMAIN }); logger.warn(err, DOMAIN); }); } /** * * @param {string} fileContent * @param {string} filepath * @return {Promise} */ function parseChunkListJson(fileContent, filepath) { return new Promise((res, rej) => { try { const chunklistJson = JSON.parse(fileContent); const tileEntities = {}; chunklistJson.map((chunkjson) => { // regionjson.map((chunkjson) => { if (Object.prototype.hasOwnProperty.call(chunkjson, "TileEntities")) { chunkjson.TileEntities.map((te) => { if (!Object.prototype.hasOwnProperty.call(tileEntities, te.id)) { tileEntities[te.id] = []; } tileEntities[te.id].push(te); }); } // }); }); return res(tileEntities); } catch (e) { logger.warn(`Unable to parse JSON from ${filepath}`, { domain: DOMAIN }); logger.debug(e, { domain: DOMAIN }); return rej(e); } }); } /** * * @param {string} filepath * @return {Promise} */ function readChunkListJson(filepath) { return fs.promises.readFile(filepath).then((filecontent) => { return parseChunkListJson(filecontent, filepath); }); } /** * @param {string} mcaJsonDir * @return {Promise} */ function buildTileEntityList(mcaJsonDir) { if (!fs.existsSync(mcaJsonDir)) fs.mkdirSync(mcaJsonDir); const jsonregionFiles = fs.readdirSync(mcaJsonDir); return Promise.all( jsonregionFiles.map((filename) => { return readChunkListJson(path.join(mcaJsonDir, filename)); }) ); }