mc-java-core-333
Version:
A better library starting minecraft game NW.js and Electron.js
395 lines (394 loc) • 18.8 kB
JavaScript
/**
* This code is distributed under the CC-BY-NC 4.0 license:
* https://creativecommons.org/licenses/by-nc/4.0/
*
* Original author: Luuxis
* Fork author: Benjas333
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const events_1 = require("events");
const node_fetch_1 = __importDefault(require("node-fetch"));
const Index_js_1 = require("../../../utils/Index.js");
const Downloader_js_1 = __importDefault(require("../../../utils/Downloader.js"));
const patcher_js_1 = __importDefault(require("../../patcher.js"));
/**
* Maps Node.js process.platform values to Mojang library naming conventions.
* Used for choosing the right native library.
*/
const Lib = {
win32: 'windows',
darwin: 'osx',
linux: 'linux'
};
/**
* The main class for handling Forge installations, including:
* - Downloading the appropriate Forge installer
* - Extracting relevant files from the installer
* - Patching Forge when necessary
* - Creating a merged jar for older Forge versions
*/
class ForgeMC extends events_1.EventEmitter {
constructor(options) {
super();
this.options = options;
}
/**
* Downloads the Forge installer (or client/universal) for the specified version/build.
* Verifies the downloaded file's MD5 hash. Returns file details or an error.
*
* @param Loader An object containing URLs for metadata and Forge downloadList.
*/
async downloadInstaller(Loader) {
// Fetch metadata for the given Forge version
let metaDataList = await (0, node_fetch_1.default)(Loader.metaData)
.then(res => res.json())
.then(json => json[this.options.loader.version]);
if (!metaDataList) {
return { error: `Forge ${this.options.loader.version} not supported` };
}
const allBuilds = metaDataList;
let build;
// Handle "latest" or "recommended" builds by checking promotions
if (this.options.loader.build === 'latest') {
let promotions = await (0, node_fetch_1.default)(Loader.promotions).then(res => res.json());
const promoKey = `${this.options.loader.version}-latest`;
const promoBuild = promotions.promos[promoKey];
build = metaDataList.find(b => b.includes(promoBuild));
}
else if (this.options.loader.build === 'recommended') {
let promotions = await (0, node_fetch_1.default)(Loader.promotions).then(res => res.json());
let promoKey = `${this.options.loader.version}-recommended`;
let promoBuild = promotions.promos[promoKey] || promotions.promos[`${this.options.loader.version}-latest`];
build = metaDataList.find(b => b.includes(promoBuild));
}
else {
// Else, look for a specific numeric build if provided
build = this.options.loader.build;
}
const chosenBuild = metaDataList.find(b => b === build);
if (!chosenBuild) {
return {
error: `Build ${build} not found, Available builds: ${allBuilds.join(', ')}`
};
}
// Fetch info about the chosen build from the meta URL
const meta = await (0, node_fetch_1.default)(Loader.meta.replace(/\${build}/g, chosenBuild)).then(res => res.json());
// Determine which classifier to use (installer, client, or universal)
const hasInstaller = meta.classifiers.installer;
const hasClient = meta.classifiers.client;
const hasUniversal = meta.classifiers.universal;
let forgeURL = '';
let ext = '';
let hashFileOrigin = '';
if (hasInstaller) {
forgeURL = Loader.install.replace(/\${version}/g, chosenBuild);
ext = Object.keys(meta.classifiers.installer)[0];
hashFileOrigin = meta.classifiers.installer[ext];
}
else if (hasClient) {
forgeURL = Loader.client.replace(/\${version}/g, chosenBuild);
ext = Object.keys(meta.classifiers.client)[0];
hashFileOrigin = meta.classifiers.client[ext];
}
else if (hasUniversal) {
forgeURL = Loader.universal.replace(/\${version}/g, chosenBuild);
ext = Object.keys(meta.classifiers.universal)[0];
hashFileOrigin = meta.classifiers.universal[ext];
}
else {
return { error: 'Invalid forge installer' };
}
const forgeFolder = path_1.default.resolve(this.options.path, 'forge');
const fileName = `${forgeURL}.${ext}`.split('/').pop();
const installerPath = path_1.default.resolve(forgeFolder, fileName);
// Download if not already present
if (!fs_1.default.existsSync(installerPath)) {
if (!fs_1.default.existsSync(forgeFolder)) {
fs_1.default.mkdirSync(forgeFolder, { recursive: true });
}
const dl = new Downloader_js_1.default();
dl.on('progress', (downloaded, size) => {
this.emit('progress', downloaded, size, fileName);
});
await dl.downloadFile(`${forgeURL}.${ext}`, forgeFolder, fileName);
}
// Verify the MD5 hash
const hashFileDownload = await (0, Index_js_1.getFileHash)(installerPath, 'md5');
if (hashFileDownload !== hashFileOrigin) {
fs_1.default.rmSync(installerPath);
return { error: 'Invalid hash' };
}
return {
filePath: installerPath,
metaData: chosenBuild,
ext,
id: `forge-${build}`
};
}
/**
* Extracts the main Forge profile from the installer's archive (install_profile.json),
* plus an additional JSON if specified in that profile. Returns an object containing
* both "install" and "version" data for further processing.
*
* @param pathInstaller Path to the downloaded Forge installer file.
*/
async extractProfile(pathInstaller) {
const fileContent = await (0, Index_js_1.getFileFromArchive)(pathInstaller, 'install_profile.json');
if (!fileContent) {
return { error: { message: 'Invalid forge installer' } };
}
const forgeJsonOrigin = JSON.parse(fileContent.toString());
if (!forgeJsonOrigin) {
return { error: { message: 'Invalid forge installer' } };
}
const result = {};
// Distinguish between older and newer Forge installers
if (forgeJsonOrigin.install) {
result.install = forgeJsonOrigin.install;
result.version = forgeJsonOrigin.versionInfo;
}
else {
result.install = forgeJsonOrigin;
const extraFile = await (0, Index_js_1.getFileFromArchive)(pathInstaller, path_1.default.basename(result.install.json));
if (!extraFile) {
return { error: { message: 'Invalid additional JSON in forge installer' } };
}
result.version = JSON.parse(extraFile.toString());
}
return result;
}
/**
* Extracts the "universal" Forge jar (or other relevant data) from the installer,
* placing it in your local "libraries" folder. Also extracts client data if required.
*
* @param profile The Forge profile object containing file paths to extract.
* @param pathInstaller The path to the Forge installer file.
* @returns A boolean (skipForgeFilter) that indicates whether to filter out certain Forge libs
*/
async extractUniversalJar(profile, pathInstaller) {
let skipForgeFilter = true;
// If there's a direct file path, extract just that file
if (profile.filePath) {
const fileInfo = (0, Index_js_1.getPathLibraries)(profile.path);
this.emit('extract', `Extracting ${fileInfo.name}...`);
const destFolder = path_1.default.resolve(this.options.path, 'libraries', fileInfo.path);
if (!fs_1.default.existsSync(destFolder)) {
fs_1.default.mkdirSync(destFolder, { recursive: true });
}
const archiveContent = await (0, Index_js_1.getFileFromArchive)(pathInstaller, profile.filePath);
if (archiveContent) {
fs_1.default.writeFileSync(path_1.default.join(destFolder, fileInfo.name), archiveContent, { mode: 0o777 });
}
}
// Otherwise, if there's a path referencing "maven/<something>"
else if (profile.path) {
const fileInfo = (0, Index_js_1.getPathLibraries)(profile.path);
const filesInArchive = await (0, Index_js_1.getFileFromArchive)(pathInstaller, null, `maven/${fileInfo.path}`);
for (const file of filesInArchive) {
const fileName = path_1.default.basename(file);
this.emit('extract', `Extracting ${fileName}...`);
const fileContent = await (0, Index_js_1.getFileFromArchive)(pathInstaller, file);
if (!fileContent) {
continue;
}
const destFolder = path_1.default.resolve(this.options.path, 'libraries', fileInfo.path);
if (!fs_1.default.existsSync(destFolder)) {
fs_1.default.mkdirSync(destFolder, { recursive: true });
}
fs_1.default.writeFileSync(path_1.default.join(destFolder, fileName), fileContent, { mode: 0o777 });
}
}
else {
// If we do not find filePath or path in profile, skip the Forge filter
skipForgeFilter = false;
}
// If there are processors, we likely have a "client.lzma" to store
if (profile.processors?.length) {
const universalPath = profile.libraries?.find((v) => (v.name || '').startsWith('net.minecraftforge:forge'));
const clientData = await (0, Index_js_1.getFileFromArchive)(pathInstaller, 'data/client.lzma');
if (clientData) {
const fileInfo = (0, Index_js_1.getPathLibraries)(profile.path || universalPath.name, '-clientdata', '.lzma');
const destFolder = path_1.default.resolve(this.options.path, 'libraries', fileInfo.path);
if (!fs_1.default.existsSync(destFolder)) {
fs_1.default.mkdirSync(destFolder, { recursive: true });
}
fs_1.default.writeFileSync(path_1.default.join(destFolder, fileInfo.name), clientData, { mode: 0o777 });
this.emit('extract', `Extracting ${fileInfo.name}...`);
}
}
return skipForgeFilter;
}
/**
* Downloads all the libraries needed by the Forge profile, skipping duplicates
* and any library that is already present. Also applies optional skip logic
* for certain Forge libraries if skipForgeFilter is true.
*
* @param profile The parsed Forge profile.
* @param skipForgeFilter Whether to filter out "net.minecraftforge:forge" or "minecraftforge"
* @returns An array of the final libraries (including newly downloaded ones).
*/
async downloadLibraries(profile, skipForgeFilter) {
let libraries = profile.version?.libraries || [];
const dl = new Downloader_js_1.default();
let checkCount = 0;
const downloadList = [];
let totalSize = 0;
// Combine with any "install.libraries"
if (profile.install?.libraries)
libraries = libraries.concat(profile.install.libraries);
// Remove duplicates by name
libraries = libraries.filter((library, index, self) => index === self.findIndex(t => t.name === library.name));
// let files: any = [];
// let size = 0;
// Certain Forge libs may be skipped if skipForgeFilter is true
const skipForge = [
'net.minecraftforge:forge:',
'net.minecraftforge:minecraftforge:'
];
const emitCheck = (libName) => {
this.emit('check', checkCount++, libraries.length, 'libraries/' + libName);
};
const getLibInfo = (lib, nativeSuffix) => {
if (!lib.downloads?.artifact?.path)
return (0, Index_js_1.getPathLibraries)(lib.name, nativeSuffix ? `-${nativeSuffix}` : '');
const libSplit = lib.downloads.artifact.path.split('/');
const libName = libSplit.pop();
return {
path: lib.downloads.artifact.path.replace(`/${libName}`, ''),
name: libName,
};
};
for (const lib of libraries) {
// If skipForgeFilter is true, skip the core Forge libs
if (skipForgeFilter && skipForge.some(forgePrefix => lib.name.includes(forgePrefix))) {
// If the artifact URL is empty, we skip it
if (!lib.downloads?.artifact?.url) {
emitCheck(lib.name);
continue;
}
}
// Some libraries might need skipping altogether (e.g., OS-specific constraints)
if ((0, Index_js_1.skipLibrary)(lib)) {
emitCheck(lib.name);
continue;
}
// Check if the library includes "natives" for the current OS
const nativeSuffix = lib.natives ? lib.natives[Lib[process.platform]] : null;
const libInfo = getLibInfo(lib, nativeSuffix);
// const libInfo = getPathLibraries(lib.name, nativeSuffix ? `-${nativeSuffix}` : '');
const libFolder = path_1.default.resolve(this.options.path, 'libraries', libInfo.path);
const libFilePath = path_1.default.resolve(libFolder, libInfo.name);
let { url, fileSize } = await this.getUrlAndSize(lib, libInfo, nativeSuffix, dl);
try {
const stats = await fs_1.default.promises.stat(libFilePath);
if (stats.size >= fileSize) {
emitCheck(lib.name);
continue;
}
}
catch (error) {
}
if (!url)
return { error: `Impossible to download ${libInfo.name}` };
totalSize += fileSize;
downloadList.push({
url: url,
folder: libFolder,
path: `${libFolder}/${libInfo.name}`,
type: libInfo.name,
size: fileSize
});
emitCheck(lib.name);
}
let onError;
if (downloadList.length > 0) {
dl.on("progress", (DL, totDL, element) => {
this.emit("progress", DL, totDL, 'libraries/' + element);
});
dl.on("error", (err) => {
this.emit("error", err);
onError = { error: err };
});
await dl.downloadFileMultiple(downloadList, totalSize, this.options.downloadFileMultiple);
}
return onError || libraries;
}
async getUrlAndSize(lib, libInfo, nativeSuffix, downloader) {
if (lib.downloads?.artifact) {
const artifact = lib.downloads.artifact;
const check = await downloader.checkURL(artifact.url).then(res => res).catch(err => false);
if (check && check.status === 200 && check.size && check.size > 0) {
return { url: artifact.url, fileSize: artifact.size || check.size };
}
}
const baseURL = nativeSuffix ? `${libInfo.path}/` : `${libInfo.path}/${libInfo.name}`;
const response = await downloader.checkMirror(baseURL, Index_js_1.mirrors);
if (response) {
return { url: response.url, fileSize: response.size };
}
return { url: null, fileSize: 0 };
}
/**
* Applies any necessary patches to Forge using the `forgePatcher` class.
* If the patcher determines it's already patched, it skips.
*
* @param profile The Forge profile containing processor information
* @returns True if successful or if no patching was required
*/
async patchForge(profile) {
let response = null;
if (!profile?.processors?.length)
return true;
const patcher = new patcher_js_1.default(this.options);
// Forward patcher events
patcher.on('patch', (data) => this.emit('patch', data));
patcher.on('error', (data) => {
// this.emit('error', data);
response = { error: data };
});
if (patcher.check(profile))
return true;
const config = {
java: this.options.loader.config.javaPath,
minecraft: this.options.loader.config.minecraftJar,
minecraftJson: this.options.loader.config.minecraftJson
};
await patcher.patcher(profile, config);
return response || true;
}
/**
* For older Forge versions, merges the vanilla Minecraft jar and Forge installer files
* into a single jar. Writes a modified version.json reflecting the new Forge version.
*
* @param id The new version ID (e.g., "forge-1.12.2-14.23.5.2855")
* @param pathInstaller Path to the Forge installer
* @returns A modified version.json with an isOldForge property and a jarPath
*/
async createProfile(id, pathInstaller) {
// Gather all entries from the Forge installer and the vanilla JAR
const forgeFiles = await (0, Index_js_1.getFileFromArchive)(pathInstaller);
const vanillaJar = await (0, Index_js_1.getFileFromArchive)(this.options.loader.config.minecraftJar);
// Combine them, excluding "META-INF" from the final jar
const mergedZip = await (0, Index_js_1.createZIP)([...vanillaJar, ...forgeFiles], 'META-INF');
// Write the new combined jar to versions/<id>/<id>.jar
const destination = path_1.default.resolve(this.options.path, 'versions', id);
if (!fs_1.default.existsSync(destination)) {
fs_1.default.mkdirSync(destination, { recursive: true });
}
fs_1.default.writeFileSync(path_1.default.resolve(destination, `${id}.jar`), mergedZip, { mode: 0o777 });
// Update the version JSON
const profileData = JSON.parse(fs_1.default.readFileSync(this.options.loader.config.minecraftJson).toString());
profileData.libraries = [];
profileData.id = id;
profileData.isOldForge = true;
profileData.jarPath = path_1.default.resolve(destination, `${id}.jar`);
return profileData;
}
}
exports.default = ForgeMC;
;