mirror of
https://github.com/artegoser/pimi-launcher-core.git
synced 2024-11-22 12:16:21 +03:00
556 lines
22 KiB
JavaScript
556 lines
22 KiB
JavaScript
const fs = require('fs');
|
|
const shelljs = require('shelljs');
|
|
const path = require('path');
|
|
const request = require('request');
|
|
const checksum = require('checksum');
|
|
const zip = require('adm-zip');
|
|
const child = require('child_process');
|
|
let counter = 0;
|
|
|
|
class Handler {
|
|
constructor(client) {
|
|
this.client = client;
|
|
this.options = client.options;
|
|
this.version = undefined;
|
|
this.baseRequest = request.defaults({
|
|
pool: {maxSockets: this.options.overrides.maxSockets || 2},
|
|
timeout: this.options.timeout || 10000
|
|
});
|
|
}
|
|
|
|
checkJava(java) {
|
|
return new Promise(resolve => {
|
|
child.exec(`"${java}" -version`, (error, stdout, stderr) => {
|
|
if (error) {
|
|
resolve({
|
|
run: false,
|
|
message: error
|
|
})
|
|
} else {
|
|
this.client.emit('debug', `[MCLC]: Using Java version ${stderr.match(/"(.*?)"/).pop()} ${stderr.includes('64-Bit') ? '64-bit': '32-Bit'}`);
|
|
resolve({
|
|
run: true
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
downloadAsync(url, directory, name, retry, type) {
|
|
return new Promise(resolve => {
|
|
shelljs.mkdir('-p', directory);
|
|
|
|
const _request = this.baseRequest(url);
|
|
|
|
let received_bytes = 0;
|
|
let total_bytes = 0;
|
|
|
|
_request.on('response', (data) => {
|
|
if (data.statusCode === 404) {
|
|
this.client.emit('debug', `[MCLC]: Failed to download ${url} due to: File not found...`);
|
|
resolve(false);
|
|
}
|
|
|
|
total_bytes = parseInt(data.headers['content-length']);
|
|
});
|
|
|
|
_request.on('error', async (error) => {
|
|
this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${error}.` +
|
|
` Retrying... ${retry}`);
|
|
if (retry) await this.downloadAsync(url, directory, name, false, type);
|
|
resolve();
|
|
});
|
|
|
|
_request.on('data', (data) => {
|
|
received_bytes += data.length;
|
|
this.client.emit('download-status', {
|
|
"name": name,
|
|
"type": type,
|
|
"current": received_bytes,
|
|
"total": total_bytes
|
|
})
|
|
});
|
|
|
|
const file = fs.createWriteStream(path.join(directory, name));
|
|
_request.pipe(file);
|
|
|
|
file.once('finish', () => {
|
|
this.client.emit('download', name);
|
|
resolve({
|
|
failed: false,
|
|
asset: null
|
|
});
|
|
});
|
|
|
|
file.on('error', async (e) => {
|
|
this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}.` +
|
|
` Retrying... ${retry}`);
|
|
if (fs.existsSync(path.join(directory, name))) shelljs.rm(path.join(directory, name));
|
|
if (retry) await this.downloadAsync(url, directory, name, false, type);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
checkSum(hash, file) {
|
|
return new Promise(resolve => {
|
|
checksum.file(file, (err, sum) => resolve(hash === sum));
|
|
});
|
|
}
|
|
|
|
getVersion() {
|
|
return new Promise(resolve => {
|
|
const versionJsonPath = this.options.overrides.versionJson || path.join(this.options.directory, `${this.options.version.number}.json`);
|
|
if (fs.existsSync(versionJsonPath)) {
|
|
this.version = JSON.parse(fs.readFileSync(versionJsonPath));
|
|
resolve(this.version);
|
|
return;
|
|
}
|
|
|
|
const manifest = `${this.options.overrides.url.meta}/mc/game/version_manifest.json`;
|
|
request.get(manifest, (error, response, body) => {
|
|
if (error) resolve(error);
|
|
|
|
const parsed = JSON.parse(body);
|
|
|
|
for (const desiredVersion in parsed.versions) {
|
|
if (parsed.versions[desiredVersion].id === this.options.version.number) {
|
|
request.get(parsed.versions[desiredVersion].url, (error, response, body) => {
|
|
if (error) resolve(error);
|
|
|
|
this.client.emit('debug', `[MCLC]: Parsed version from version manifest`);
|
|
this.version = JSON.parse(body);
|
|
resolve(this.version);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
getJar() {
|
|
return new Promise(async (resolve) => {
|
|
await this.downloadAsync(this.version.downloads.client.url, this.options.directory, `${this.options.version.number}.jar`, true, 'version-jar');
|
|
|
|
fs.writeFileSync(path.join(this.options.directory, `${this.options.version.number}.json`), JSON.stringify(this.version, null, 4));
|
|
|
|
this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json');
|
|
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
getAssets() {
|
|
return new Promise(async(resolve) => {
|
|
if(!fs.existsSync(path.join(this.options.root, 'assets', 'indexes', `${this.version.assetIndex.id}.json`))) {
|
|
await this.downloadAsync(this.version.assetIndex.url, path.join(this.options.root, 'assets', 'indexes'),
|
|
`${this.version.assetIndex.id}.json`, true, 'asset-json');
|
|
}
|
|
|
|
const index = require(path.join(this.options.root, 'assets', 'indexes',`${this.version.assetIndex.id}.json`));
|
|
|
|
this.client.emit('progress', {
|
|
type: 'assets',
|
|
task: 0,
|
|
total: Object.keys(index.objects).length
|
|
});
|
|
|
|
await Promise.all(Object.keys(index.objects).map(async asset => {
|
|
const hash = index.objects[asset].hash;
|
|
const subhash = hash.substring(0,2);
|
|
const assetDirectory = this.options.overrides.assetRoot || path.join(this.options.root, 'assets');
|
|
const subAsset = path.join(assetDirectory, 'objects', subhash);
|
|
|
|
if(!fs.existsSync(path.join(subAsset, hash)) || !await this.checkSum(hash, path.join(subAsset, hash))) {
|
|
await this.downloadAsync(`${this.options.overrides.url.resource}/${subhash}/${hash}`, subAsset, hash,
|
|
true, 'assets');
|
|
counter = counter + 1;
|
|
this.client.emit('progress', {
|
|
type: 'assets',
|
|
task: counter,
|
|
total: Object.keys(index.objects).length
|
|
})
|
|
}
|
|
}));
|
|
counter = 0;
|
|
|
|
// Copy assets to legacy if it's an older Minecraft version.
|
|
if(this.version.assets === "legacy" || this.version.assets === "pre-1.6") {
|
|
const assetDirectory = this.options.overrides.assetRoot || path.join(this.options.root, 'assets');
|
|
this.client.emit('debug', `[MCLC]: Copying assets over to ${path.join(assetDirectory, 'legacy')}`);
|
|
|
|
this.client.emit('progress', {
|
|
type: 'assets-copy',
|
|
task: 0,
|
|
total: Object.keys(index.objects).length
|
|
});
|
|
|
|
await Promise.all(Object.keys(index.objects).map(async asset => {
|
|
const hash = index.objects[asset].hash;
|
|
const subhash = hash.substring(0,2);
|
|
const subAsset = path.join(assetDirectory, 'objects', subhash);
|
|
|
|
let legacyAsset = asset.split('/');
|
|
legacyAsset.pop();
|
|
|
|
if(!fs.existsSync(path.join(assetDirectory, 'legacy', legacyAsset.join('/')))) {
|
|
shelljs.mkdir('-p', path.join(assetDirectory, 'legacy', legacyAsset.join('/')));
|
|
}
|
|
|
|
if (!fs.existsSync(path.join(assetDirectory, 'legacy', asset))) {
|
|
fs.copyFileSync(path.join(subAsset, hash), path.join(assetDirectory, 'legacy', asset))
|
|
}
|
|
counter = counter + 1;
|
|
this.client.emit('progress', {
|
|
type: 'assets-copy',
|
|
task: counter,
|
|
total: Object.keys(index.objects).length
|
|
})
|
|
}));
|
|
}
|
|
counter = 0;
|
|
|
|
this.client.emit('debug', '[MCLC]: Downloaded assets');
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
parseRule(lib) {
|
|
if(lib.rules) {
|
|
if (lib.rules.length > 1) {
|
|
if (lib.rules[0].action === 'allow' &&
|
|
lib.rules[1].action === 'disallow' &&
|
|
lib.rules[1].os.name === 'osx') {
|
|
return this.getOS() === 'osx';
|
|
} else {
|
|
return true;
|
|
}
|
|
} else {
|
|
if (lib.rules[0].action === 'allow' && lib.rules[0].os) return this.getOS() !== 'osx';
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
getNatives() {
|
|
return new Promise(async(resolve) => {
|
|
const nativeDirectory = this.options.overrides.natives || path.join(this.options.root, 'natives', this.version.id);
|
|
|
|
if(!fs.existsSync(nativeDirectory) || !fs.readdirSync(nativeDirectory).length) {
|
|
shelljs.mkdir('-p', nativeDirectory);
|
|
|
|
const natives = () => {
|
|
return new Promise(async resolve => {
|
|
const natives = [];
|
|
await Promise.all(this.version.libraries.map(async (lib) => {
|
|
if (!lib.downloads.classifiers) return;
|
|
if (this.parseRule(lib)) return;
|
|
|
|
const native = this.getOS() === 'osx' ?
|
|
lib.downloads.classifiers['natives-osx'] || lib.downloads.classifiers['natives-macos'] :
|
|
lib.downloads.classifiers[`natives-${this.getOS()}`];
|
|
|
|
natives.push(native);
|
|
}));
|
|
resolve (natives);
|
|
})
|
|
};
|
|
const stat = await natives();
|
|
|
|
this.client.emit('progress', {
|
|
type: 'natives',
|
|
task: 0,
|
|
total: stat.length
|
|
});
|
|
|
|
await Promise.all(stat.map(async (native) => {
|
|
const name = native.path.split('/').pop();
|
|
await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives');
|
|
if (!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) {
|
|
await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives');
|
|
}
|
|
try {
|
|
new zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true);
|
|
} catch (e) {
|
|
// Only doing a console.warn since a stupid error happens. You can basically ignore this.
|
|
// if it says Invalid file name, just means two files were downloaded and both were deleted.
|
|
// All is well.
|
|
console.warn(e);
|
|
}
|
|
shelljs.rm(path.join(nativeDirectory, name));
|
|
counter = counter + 1;
|
|
this.client.emit('progress', {
|
|
type: 'natives',
|
|
task: counter,
|
|
total: stat.length
|
|
})
|
|
}));
|
|
this.client.emit('debug', '[MCLC]: Downloaded and extracted natives');
|
|
}
|
|
|
|
counter = 0;
|
|
this.client.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`);
|
|
resolve(nativeDirectory);
|
|
});
|
|
}
|
|
|
|
async getForgeDependenciesLegacy() {
|
|
if(!fs.existsSync(path.join(this.options.root, 'forge'))) {
|
|
shelljs.mkdir('-p', path.join(this.options.root, 'forge'));
|
|
}
|
|
|
|
try {
|
|
await new zip(this.options.forge).extractEntryTo('version.json', path.join(this.options.root, 'forge', `${this.version.id}`), false, true);
|
|
} catch(e) {
|
|
this.client.emit('debug', `[MCLC]: Unable to extract version.json from the forge jar due to ${e}`);
|
|
return null;
|
|
}
|
|
|
|
const forge = require(path.join(this.options.root, 'forge', `${this.version.id}`, 'version.json'));
|
|
const paths = [];
|
|
|
|
this.client.emit('progress', {
|
|
type: 'forge',
|
|
task: 0,
|
|
total: forge.libraries.length
|
|
});
|
|
|
|
await Promise.all(forge.libraries.map(async library => {
|
|
const lib = library.name.split(':');
|
|
|
|
if(lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return;
|
|
|
|
let url = this.options.overrides.url.mavenForge;
|
|
const jarPath = path.join(this.options.root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`);
|
|
const name = `${lib[1]}-${lib[2]}.jar`;
|
|
|
|
if(!library.url) {
|
|
if(library.serverreq || library.clientreq) {
|
|
url = this.options.overrides.url.defaultRepoForge;
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
|
|
const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}`;
|
|
|
|
|
|
if(fs.existsSync(path.join(jarPath, name))) {
|
|
paths.push(`${jarPath}${path.sep}${name}`);
|
|
counter = counter + 1;
|
|
this.client.emit('progress', { type: 'forge', task: counter, total: forge.libraries.length});
|
|
return;
|
|
}
|
|
if(!fs.existsSync(jarPath)) shelljs.mkdir('-p', jarPath);
|
|
|
|
const download = await this.downloadAsync(downloadLink, jarPath, name, true, 'forge');
|
|
if(!download) await this.downloadAsync(`${this.options.overrides.url.fallbackMaven}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}`, jarPath, name, true, 'forge');
|
|
|
|
paths.push(`${jarPath}${path.sep}${name}`);
|
|
counter = counter + 1;
|
|
this.client.emit('progress', {
|
|
type: 'forge',
|
|
task: counter,
|
|
total: forge.libraries.length
|
|
})
|
|
}));
|
|
|
|
counter = 0;
|
|
this.client.emit('debug', '[MCLC]: Downloaded Forge dependencies');
|
|
|
|
return {paths, forge};
|
|
}
|
|
|
|
runInstaller(path) {
|
|
return new Promise(resolve => {
|
|
const installer = child.exec(path);
|
|
installer.on('close', (code) => resolve());
|
|
})
|
|
}
|
|
|
|
getClasses() {
|
|
return new Promise(async (resolve) => {
|
|
const libs = [];
|
|
|
|
if(this.options.version.custom) {
|
|
const customJarJson = require(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`));
|
|
|
|
this.client.emit('progress', {
|
|
type: 'classes-custom',
|
|
task: 0,
|
|
total: customJarJson.libraries.length
|
|
});
|
|
|
|
await Promise.all(customJarJson.libraries.map(async library => {
|
|
const lib = library.name.split(':');
|
|
|
|
const jarPath = path.join(this.options.root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`);
|
|
const name = `${lib[1]}-${lib[2]}.jar`;
|
|
|
|
if(!fs.existsSync(path.join(jarPath, name))) {
|
|
if(library.url) {
|
|
const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${lib[1]}-${lib[2]}.jar`;
|
|
await this.downloadAsync(url, jarPath, name, true, 'classes-custom');
|
|
}
|
|
}
|
|
counter = counter + 1;
|
|
this.client.emit('progress', {
|
|
type: 'classes-custom',
|
|
task: counter,
|
|
total: customJarJson.libraries.length
|
|
});
|
|
libs.push(`${jarPath}${path.sep}${name}`);
|
|
}));
|
|
counter = 0;
|
|
}
|
|
|
|
const parsedClasses = () => {
|
|
return new Promise(async resolve => {
|
|
const classes = [];
|
|
await Promise.all(this.version.libraries.map(async (_lib) => {
|
|
if(!_lib.downloads.artifact) return;
|
|
if(this.parseRule(_lib)) return;
|
|
|
|
classes.push(_lib);
|
|
}));
|
|
resolve(classes);
|
|
})
|
|
};
|
|
const parsed = await parsedClasses();
|
|
|
|
this.client.emit('progress', {
|
|
type: 'classes',
|
|
task: 0,
|
|
total: parsed.length
|
|
});
|
|
|
|
await Promise.all(parsed.map(async (_lib) => {
|
|
const libraryPath = _lib.downloads.artifact.path;
|
|
const libraryUrl = _lib.downloads.artifact.url;
|
|
const libraryHash = _lib.downloads.artifact.sha1;
|
|
const libraryDirectory = path.join(this.options.root, 'libraries', libraryPath);
|
|
|
|
if(!fs.existsSync(libraryDirectory) || !await this.checkSum(libraryHash, libraryDirectory)) {
|
|
let directory = libraryDirectory.split(path.sep);
|
|
const name = directory.pop();
|
|
directory = directory.join(path.sep);
|
|
|
|
await this.downloadAsync(libraryUrl, directory, name, true, 'classes');
|
|
}
|
|
counter = counter + 1;
|
|
this.client.emit('progress', {
|
|
type: 'classes',
|
|
task: counter,
|
|
total: parsed.length
|
|
});
|
|
libs.push(libraryDirectory);
|
|
}));
|
|
counter = 0;
|
|
|
|
this.client.emit('debug', '[MCLC]: Collected class paths');
|
|
resolve(libs)
|
|
});
|
|
}
|
|
|
|
static cleanUp(array) {
|
|
return new Promise(resolve => {
|
|
const newArray = [];
|
|
|
|
for(let classPath in array) {
|
|
if(newArray.includes(array[classPath])) continue;
|
|
newArray.push(array[classPath]);
|
|
}
|
|
resolve(newArray);
|
|
})
|
|
}
|
|
|
|
getLaunchOptions(modification) {
|
|
return new Promise(async resolve => {
|
|
let type = modification || this.version;
|
|
|
|
let args = type.minecraftArguments ? type.minecraftArguments.split(' ') : type.arguments.game;
|
|
const assetRoot = this.options.overrides.assetRoot || path.join(this.options.root, 'assets');
|
|
const assetPath = this.version.assets === "legacy" || this.version.assets === "pre-1.6" ? path.join(assetRoot, 'legacy') : path.join(assetRoot);
|
|
|
|
const minArgs = this.options.overrides.minArgs || 5;
|
|
if(args.length < minArgs) args = args.concat(this.version.minecraftArguments ? this.version.minecraftArguments.split(' ') : this.version.arguments.game);
|
|
|
|
this.options.authorization = await Promise.resolve(this.options.authorization);
|
|
|
|
const fields = {
|
|
'${auth_access_token}': this.options.authorization.access_token,
|
|
'${auth_session}': this.options.authorization.access_token,
|
|
'${auth_player_name}': this.options.authorization.name,
|
|
'${auth_uuid}': this.options.authorization.uuid,
|
|
'${user_properties}': this.options.authorization.user_properties,
|
|
'${user_type}': 'mojang',
|
|
'${version_name}': this.options.version.number,
|
|
'${assets_index_name}': this.version.assetIndex.id,
|
|
'${game_directory}': this.options.root,
|
|
'${assets_root}': assetPath,
|
|
'${game_assets}': assetPath,
|
|
'${version_type}': this.options.version.type
|
|
};
|
|
|
|
for (let index = 0; index < args.length; index++) {
|
|
if(typeof args[index] === 'object') args.splice(index, 2);
|
|
if (Object.keys(fields).includes(args[index])) {
|
|
args[index] = fields[args[index]];
|
|
}
|
|
}
|
|
|
|
if(this.options.window) this.options.window.fullscreen ? args.push('--fullscreen') : args.push('--width', this.options.window.width, '--height', this.options.window.height);
|
|
if(this.options.server) args.push('--server', this.options.server.host, '--port', this.options.server.port || "25565");
|
|
if(this.options.proxy) args.push(
|
|
'--proxyHost',
|
|
this.options.proxy.host,
|
|
'--proxyPort',
|
|
this.options.proxy.port || "8080",
|
|
'--proxyUser',
|
|
this.options.proxy.username,
|
|
'--proxyPass',
|
|
this.options.proxy.password
|
|
);
|
|
|
|
this.client.emit('debug', '[MCLC]: Set launch options');
|
|
resolve(args);
|
|
});
|
|
}
|
|
|
|
async getJVM() {
|
|
const opts = {
|
|
"windows": "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump",
|
|
"osx": "-XstartOnFirstThread",
|
|
"linux": "-Xss1M"
|
|
};
|
|
return opts[this.getOS()]
|
|
}
|
|
|
|
getOS() {
|
|
if(this.options.os) {
|
|
return this.options.os;
|
|
} else {
|
|
switch(process.platform) {
|
|
case "win32": return "windows";
|
|
case "darwin": return "osx";
|
|
default: return "linux";
|
|
}
|
|
}
|
|
}
|
|
|
|
extractPackage(options = this.options) {
|
|
return new Promise(async resolve => {
|
|
if(options.clientPackage.startsWith('http')) {
|
|
await this.downloadAsync(options.clientPackage, options.root, "clientPackage.zip", true, 'client-package');
|
|
options.clientPackage = path.join(options.root, "clientPackage.zip")
|
|
}
|
|
new zip(options.clientPackage).extractAllTo(options.root, true);
|
|
this.client.emit('package-extract', true);
|
|
if(options.removePackage) shelljs.rm(options.clientPackage);
|
|
resolve();
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = Handler;
|