Skip to content

Instantly share code, notes, and snippets.

@benhatsor
Last active April 4, 2026 20:50
Show Gist options
  • Select an option

  • Save benhatsor/befd30f8c4b623fd641671c6dabbf7d3 to your computer and use it in GitHub Desktop.

Select an option

Save benhatsor/befd30f8c4b623fd641671c6dabbf7d3 to your computer and use it in GitHub Desktop.
Simple Minecraft Decompiler.
/**
* Simple Minecraft Decompiler
*
* Usage:
* - Decompile latest version: `node simple-mc-decompiler.js`
* - To specify version: `node simple-mc-decompiler.js [version]`
* - version = `latest` or `latest-release` / `latest-snapshot` / [version name]
* - Add `-s` or `--server` to decompile server
*/
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { PassThrough } from 'node:stream';
import readline from 'node:readline';
import path from 'node:path';
import os from 'node:os';
const serverArgAliases = ['-s', '--server'];
async function main() {
// remove initial "node decompile.js" args
// and clone array
let args = process.argv.slice(2);
let versionParam = 'latest';
if (args[0] && !serverArgAliases.includes(args[0])) {
versionParam = args[0];
args.shift();
}
const isServer = args.some(arg =>
serverArgAliases.includes(arg)
);
const serverLog = isServer ? ' (server)' : '';
console.info(`Fetching version '${versionParam}'${serverLog}...`);
const versionObj = await getVersionObj({
versionParam
});
const versionJarURL = await getVersionJarURL({
versionObj,
isServer
});
await using tempDir = await fs.mkdtempDisposable(
path.join(os.tmpdir(), 'decompiler-') // 'decompiler-'
);
console.info(`Downloading ${versionObj.id}${serverLog} jar...`);
const serverSuffix = isServer ? '-server' : '';
const versionJarName = `${versionObj.id}${serverSuffix}.jar`;
let versionJarPath = path.join(tempDir.path, versionJarName);
await downloadFile({
at: versionJarURL,
to: versionJarPath
});
if (isServer) {
console.info(`Extracting server jar from bundle...`);
const extractedBundleJarFolderName = `extractedBundle-${versionObj.id}${serverSuffix}`;
const extractedBundleJarPath = path.join(tempDir.path, extractedBundleJarFolderName);
const serverJarInnerPath = path.join(
'META-INF', 'versions', versionObj.id, `server-${versionObj.id}.jar`
);
const serverJarPath = path.join(extractedBundleJarPath, serverJarInnerPath);
await createDirIfNeccessary(extractedBundleJarPath);
const { output, exitPromise } = executeCommand({
command: `jar`,
args: [`xf`, path.resolve(versionJarPath), serverJarInnerPath],
options: {
cwd: extractedBundleJarPath
}
});
await logOverwritingOutput(output);
await exitPromise;
if (existsSync(serverJarPath)) {
versionJarPath = serverJarPath;
} else {
console.info(`Couldn't extract server jar. Decompiling entire bundle instead.`);
}
}
console.info(`Fetching latest Vineflower release...`);
const vfReleaseResp = await fetch(`https://api.github.com/repos/Vineflower/vineflower/releases/latest`);
const vfRelease = await vfReleaseResp.json();
console.info(`Downloading Vineflower ${vfRelease.tag_name}...`);
const vineflowerJarName = `vineflower-${vfRelease.tag_name}.jar`;
const vineflowerJarPath = path.join(tempDir.path, vineflowerJarName);
const vfReleaseJar = vfRelease.assets.find(asset => asset.name === vineflowerJarName);
const vfReleaseJarURL = vfReleaseJar.browser_download_url;
await downloadFile({
at: vfReleaseJarURL,
to: vineflowerJarPath
});
console.info(`Decompiling ${versionObj.id}${serverLog} jar...`);
const outputFolderName = `decompiled-${versionObj.id}${serverSuffix}`;
const outputPath = path.resolve(outputFolderName);
await createDirIfNeccessary(outputPath);
await decompileFile({
vineflowerJarPath, versionJarPath, outputPath
});
console.info(`Decompiled succesfully to: ${outputPath}`);
}
async function decompileFile({ vineflowerJarPath, versionJarPath, outputPath }) {
const { output, exitPromise } = executeCommand({
command: `java`,
args: [`-jar`, vineflowerJarPath, `-dgs=1`, `-asc=1`, versionJarPath, outputPath]
});
await logOverwritingOutput(output);
await exitPromise;
const nestedJars = await findJars(outputPath);
for (const jarPath of nestedJars) {
const jarOutputPath = jarPath.replace(/\.jar$/, '');
await createDirIfNeccessary(jarOutputPath);
await decompileFile({
vineflowerJarPath,
versionJarPath: jarPath,
outputPath: jarOutputPath
});
await fs.rm(jarPath);
}
}
async function findJars(dir) {
const results = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...await findJars(fullPath));
} else if (entry.name.endsWith('.jar')) {
results.push(fullPath);
}
}
return results;
}
const mcVersionManifestURL =
'https://piston-meta.mojang.com/mc/game/version_manifest.json';
const versionKeywords = {
latest: {
release: ['latest', 'latest-release'],
snapshot: ['latest-snapshot'],
}
};
async function getVersionObj({
versionParam = 'latest'
}) {
const versionManifestResp = await fetch(mcVersionManifestURL);
const versionManifest = await versionManifestResp.json();
let versionId = versionParam;
if (versionKeywords.latest.release.includes(versionParam)) {
versionId = versionManifest.latest.release;
} else if (versionKeywords.latest.snapshot.includes(versionParam)) {
versionId = versionManifest.latest.snapshot;
}
const versionObj = versionManifest.versions.find(version =>
version.id === versionId
);
if (!versionObj) {
throw new Error(`Version dosen't exist.`);
}
return versionObj;
}
async function getVersionJarURL({ versionObj, isServer = false }) {
const versionResp = await fetch(versionObj.url);
const version = await versionResp.json();
const versionType = isServer ? 'server' : 'client';
const versionJarURL = version.downloads[versionType].url;
return versionJarURL;
}
async function downloadFile({ at: fileURL, to: filePath }) {
const response = await fetch(fileURL);
if (!response.ok || !response.body) {
throw new Error(`Couldn't fetch file.`);
}
const totalBytes = Number(response.headers.get('content-length')) || 0;
let downloadedBytes = 0;
const fileHandle = await fs.open(filePath, 'w');
try {
for await (const chunk of response.body) {
await fileHandle.write(chunk);
downloadedBytes += chunk.byteLength;
if (totalBytes > 0) {
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
const mb = (downloadedBytes / 1e6).toFixed(2);
const totalMb = (totalBytes / 1e6).toFixed(2);
process.stdout.write(`\r ${mb} MB / ${totalMb} MB (${percent}%)`);
}
}
process.stdout.write('\n');
} finally {
await fileHandle.close();
}
}
async function createDirIfNeccessary(path) {
if (!existsSync(path)) {
await fs.mkdir(path, { recursive: true });
}
}
function executeCommand({ command, args = [], options = {} }) {
const process = spawn(command, args, options);
const output = new PassThrough();
output.setEncoding('utf8');
process.stdout.pipe(output);
process.stderr.pipe(output);
const { promise: exitPromise, resolve, reject } = Promise.withResolvers();
process.on('error', reject);
process.on('close', (code) => {
if (code !== 0) reject(new Error(`Process exited with code ${code}`));
else resolve();
});
return {
process,
output,
exitPromise
};
}
async function logOverwritingOutput(output) {
for await (const data of output) {
const lines = data.trim().split('\n');
for (const line of lines) {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
const prefix = ' ';
let trimmedLine = prefix + line.trim();
// trim line to console width
trimmedLine = trimmedLine.slice(0, process.stdout.columns);
process.stdout.write(trimmedLine);
}
}
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment