dmclc
Version:
Dolphin Minecraft Launcher Core
371 lines (370 loc) • 16.7 kB
JavaScript
import cp, { exec } from "child_process";
import compressing from "compressing";
import fs from "fs";
import { ensureDir, mkdirs } from "fs-extra";
import { readFile, writeFile } from "fs/promises";
import got from "got";
import StreamZip from "node-stream-zip";
import os from "os";
import path from "path";
import { promisify } from "util";
import { FormattedError } from "./errors/FormattedError.js";
import { ContentType, Pair } from "./index.js";
import { ModrinthContent } from "./mods/download/modrinth/ModrinthContentService.js";
import { ModManager } from "./mods/manage/ModManager.js";
import { checkRules } from "./schemas.js";
import { transformURL } from "./utils/TransformURL.js";
import { checkAndDownload, checkAndDownloadAll, download } from "./utils/downloads.js";
import { expandInheritsFrom } from "./utils/expand_inherits_from.js";
import { expandMavenId } from "./utils/maven.js";
/**
* Version.
* @public
*/
export class MinecraftVersion {
launcher;
versionObject;
extras;
name;
versionRoot;
versionLaunchWorkDir;
versionJarPath;
modManager;
/**
* Creates a new version from name.
* @param launcher - The launcher instance
* @param name - The name of this version. The directory name, not always Minecraft version.
* @returns The new created version object.
*/
static fromVersionName(launcher, name, enableIndependentGameDir = false) {
const version = new MinecraftVersion(launcher, expandInheritsFrom(JSON.parse(fs.readFileSync(`${launcher.rootPath}/versions/${name}/${name}.json`).toString()), launcher.rootPath), enableIndependentGameDir);
return version;
}
/**
* Creates a new version from JSON object.
* @param launcher - The launcher instance.
* @param object - The Version JSON object.
*/
constructor(launcher, object, enableIndependentGameDir) {
this.launcher = launcher;
this.versionObject = object;
this.name = object.id;
this.versionRoot = `${this.launcher.rootPath}/versions/${this.name}`;
this.versionJarPath = `${this.versionRoot}/${this.name}.jar`;
const extraPath = `${this.versionRoot}/dmclc_extras.json`;
if (!fs.existsSync(extraPath)) {
this.extras = this.detectExtras(enableIndependentGameDir);
this.saveExtras();
}
else {
this.extras = JSON.parse(fs.readFileSync(extraPath).toString());
}
this.versionLaunchWorkDir = this.extras.enableIndependentGameDir
? this.versionRoot
: this.launcher.rootPath.toString();
this.modManager = new ModManager(this, launcher);
}
detectExtras(enableIndependentGameDir) {
const loaders = [];
let version = this.versionObject.clientVersion;
this.launcher.loaders.forEach((v, k) => {
const version = v.findInVersion(this.versionObject);
if (version) {
loaders.push({
name: k,
version: version
});
}
});
for (const v of this.versionObject.libraries) {
if (v.name.includes(":forge:") || v.name.includes(":fmlloader:") || v.name.includes(":liteloader:") || v.name.includes(":intermediary:")) {
version = v.name.split(":")[2].split("-")[0];
break;
}
}
if (version == undefined) {
version = this.getVersionFromJar();
}
return {
version,
loaders,
enableIndependentGameDir
};
}
getVersionFromJar() {
const zip = new StreamZip({ file: this.versionJarPath });
const entry = zip.entry("version.json");
let version = "Unknown";
if (entry) {
const obj = JSON.parse((zip.entryDataSync(entry)).toString());
version = obj.id;
}
zip.close();
return version;
}
/**
* Run this version!
* @throws RequestError
* @param account - The using account.
* @returns The Minecraft process. Both stdout and stderr uses UTF-8.
*/
async run(account) {
const progress = this.launcher.createProgress(5, "version.progress.run", "version.progress.account_login");
try {
if (!await account.check()) {
await account.login();
}
progress.update("version.progress.account_prepare");
await account.prepareLaunch(this.versionLaunchWorkDir);
progress.update("version.progress.complete");
await this.completeVersionInstall(false);
progress.update("version.progress.extract_native");
await this.extractNative(this.versionObject, this.name);
progress.update("version.progress.argument");
const args = await this.getArguments(this.versionObject, account);
const allArguments = ["-Dsun.stdout.encoding=utf-8", "-Dsun.stderr.encoding=utf-8"]
.concat(await account.getLaunchJVMArgs(this))
.concat(this.extras.moreJavaArguments ?? [])
.concat(args)
.concat(this.extras.moreGameArguments ?? []);
progress.update("version.progress.done");
if (this.extras.beforeCommand)
await promisify(exec)(this.extras.beforeCommand);
return cp.execFile(this.extras.usingJava ?? this.launcher.usingJava, allArguments, {
cwd: this.versionLaunchWorkDir
});
}
finally {
progress.close();
}
}
/**
* Complete this version installation. Fix wrong libraries, asset files and version.jar. Won't fix version.json.
*/
async completeVersionInstall(alwaysDownloadNoDownloadsItems = true) {
const promises = [];
promises.push(checkAndDownload(this.versionObject.downloads.client.url, this.versionJarPath, this.versionObject.downloads.client.sha1, this.launcher));
promises.push(this.completeAssets(this.versionObject.assetIndex));
promises.push(this.completeLibraries(this.versionObject.libraries, alwaysDownloadNoDownloadsItems));
return !(await Promise.all(promises)).includes(false);
}
async completeAssets(asset) {
const allDownloads = new Map();
const indexPath = `${this.launcher.rootPath}/assets/indexes/${asset.id}.json`;
let assetJson;
if (!fs.existsSync(indexPath)) {
assetJson = (await got(transformURL(asset.url, this.launcher.mirror))).body;
await mkdirs(`${this.launcher.rootPath}/assets/indexes`);
await writeFile(indexPath, assetJson);
}
else {
assetJson = (await readFile(indexPath)).toString();
}
const assetsObjects = `${this.launcher.rootPath}/assets/objects`;
const assetobj = JSON.parse(assetJson);
for (const assid in assetobj.objects) {
const assitem = assetobj.objects[assid];
allDownloads.set(`https://resources.download.minecraft.net/${assitem.hash.slice(0, 2)}/${assitem.hash}`, new Pair(assitem.hash, `${assetsObjects}/${assitem.hash.slice(0, 2)}/${assitem.hash}`));
}
return await checkAndDownloadAll(allDownloads, this.launcher);
}
/**
* INTERNAL API. MAY BE CHANGE WITHOUT NOTIFY.
* Fix wrong and missing libraries. Used by Forge installing.
* @param liblist - All the libraries.
* @internal
*/
async completeLibraries(liblist, alwaysDownloadNoDownloadsItems = true) {
const allDownloads = new Map();
const used = liblist.filter((i) => {
return i.rules === undefined || checkRules(i.rules);
});
for (const i of used) {
if (!("downloads" in i)) {
if (alwaysDownloadNoDownloadsItems) {
const filePath = expandMavenId(i.name);
let url;
if (!("url" in i))
url = "https://libraries.minecraft.net/";
else
url = i.url;
allDownloads.set(`${url}${filePath}`, new Pair("no", `${this.launcher.rootPath}/libraries/${filePath}`));
}
}
else {
const artifacts = [];
if ("artifact" in i.downloads) {
artifacts.push(i.downloads.artifact);
}
if ("natives" in i) {
artifacts.push(i.downloads.classifiers[i.natives[this.launcher.natives].replaceAll("${arch}", os.arch().includes("64") ? "64" : "32")]);
}
for (const artifact of artifacts) {
allDownloads.set(artifact.url, new Pair(artifact.sha1, `${this.launcher.rootPath}/libraries/${artifact.path}`));
}
}
}
return await checkAndDownloadAll(allDownloads, this.launcher);
}
getClassPath(versionObject) {
const res = [];
versionObject.libraries.filter(i => i.rules === undefined || checkRules(i.rules)).forEach((i) => {
if (!("downloads" in i)) {
res.push(`${this.launcher.rootPath}${path.sep}libraries${path.sep}${expandMavenId(i.name)}`);
}
else if ("artifact" in i.downloads) {
res.push(`${this.launcher.rootPath}${path.sep}libraries${path.sep}${i.downloads.artifact.path.replaceAll("/", path.sep)}`);
}
});
res.push(this.versionJarPath);
return res;
}
parseArgument(arg, versionObject, account, argOverrides) {
let argVal;
if (typeof arg === "object") {
if (arg.value instanceof Array)
argVal = arg.value.join(" ");
else
argVal = arg.value;
}
else
argVal = arg;
argVal = argVal.replaceAll("${version_name}", `${this.name}`)
.replaceAll("${game_directory}", this.versionLaunchWorkDir)
.replaceAll("${assets_root}", `${this.launcher.rootPath}${path.sep}assets`)
.replaceAll("${assets_index_name}", versionObject.assets)
.replaceAll("${auth_uuid}", `${account.getUUID()}`)
.replaceAll("${version_type}", `${this.launcher.name}`)
.replaceAll("${natives_directory}", `${this.launcher.rootPath}${path.sep}versions${path.sep}${this.name}${path.sep}natives`)
.replaceAll("${launcher_name}", `${this.launcher.name}`)
.replaceAll("${launcher_version}", "0.1")
.replaceAll("${library_directory}", `${this.launcher.rootPath}${path.sep}libraries`)
.replaceAll("${classpath_separator}", path.delimiter)
.replaceAll("${classpath}", this.getClassPath(versionObject).join(path.delimiter));
argOverrides.forEach((v, k) => {
argVal = argVal.replaceAll("${" + k + "}", v);
});
return argVal;
}
async getArguments(versionObject, account) {
const res = [];
const args = await account.getLaunchGameArgs();
if ("arguments" in versionObject) {
versionObject.arguments.jvm?.map(async (i) => {
if (typeof (i) === "string") {
res.push(this.parseArgument(i, versionObject, account, args));
}
});
res.push(versionObject.mainClass);
versionObject.arguments.game?.map(async (i) => {
if (typeof (i) === "string") {
res.push(this.parseArgument(i, versionObject, account, args));
}
});
}
else {
res.push(`-Djava.library.path=${this.launcher.rootPath}${path.sep}versions${path.sep}${this.name}${path.sep}natives`);
res.push("-cp", this.getClassPath(versionObject).join(path.delimiter));
res.push(versionObject.mainClass);
versionObject.minecraftArguments.split(" ").map(async (i) => {
if (typeof (i) === "string") {
res.push(this.parseArgument(i, versionObject, account, args));
}
});
}
return res;
}
async extractNative(version, name) {
Promise.all(version.libraries.filter(i => i.rules === undefined || checkRules(i.rules))
.map(async (lib) => {
if (!("downloads" in lib))
return;
if (!("natives" in lib))
return;
const native = lib.downloads.classifiers[lib.natives[this.launcher.natives].replace("${arch}", os.arch().includes("64") ? "64" : "32")];
const libpath = `${this.launcher.rootPath}/libraries/${native?.path}`;
await compressing.zip.uncompress(libpath, `${this.launcher.rootPath}/versions/${name}/natives`);
}));
}
/**
* Get all the installable loader versions on this Minecraft version. Doesn't consider loader conflicts.
* @throws {@link FormattedError}
* @throws RequestError
* @param name - The name of loader.
* @returns The versions of loader.
*/
async getSuitableLoaderVersions(name) {
const loader = this.launcher.loaders.get(name);
if (loader == undefined) {
throw new FormattedError(`${this.launcher.i18n("version.loader_not_found")}: ${name}`);
}
return loader.getSuitableLoaderVersions(this);
}
/**
* Install a mod loader.
* @throws {@link FormattedError}
* @throws RequestError
* @param name - Loader name.
* @param loaderVersion - Loader version.
*/
async installLoader(name, loaderVersion) {
const loader = this.launcher.loaders.get(name);
if (loader == undefined) {
throw new FormattedError(`${this.launcher.i18n("version.loader_not_found")}: ${name}`);
}
await loader.install(this, loaderVersion);
this.extras.loaders.push({
name: name,
version: loaderVersion
});
this.saveExtras();
}
saveExtras() {
fs.writeFileSync(`${this.versionRoot}/dmclc_extras.json`, JSON.stringify(this.extras));
}
/**
* @throws RequestError
* @param contentVersion content version
*/
async installContentVersion(contentVersion) {
switch ((await contentVersion.getContent()).getType()) {
case ContentType.MOD:
await this.modManager.installContentVersion(contentVersion);
break;
case ContentType.RESOURCE_PACK:
const resourcePackPath = `${this.versionLaunchWorkDir}/resourcepacks/${await contentVersion.getVersionFileName()}`;
await download(await contentVersion.getVersionFileURL(), resourcePackPath, this.launcher);
break;
case ContentType.SHADER:
let packType = "shaders";
let content = contentVersion.getContent();
if (content instanceof ModrinthContent && content.isVanillaOrCanvasShader()) {
packType = "resourcepacks";
}
const shaderPath = `${this.versionLaunchWorkDir}/${packType}/${await contentVersion.getVersionFileName()}`;
await download(await contentVersion.getVersionFileURL(), shaderPath, this.launcher);
break;
case ContentType.MODPACK:
const packPath = `${this.launcher.envPaths.cache}/downloaded/content/${await contentVersion.getVersionFileName()}`;
if (!await checkAndDownload(await contentVersion.getVersionFileURL(), packPath, await contentVersion.getVersionFileSHA1(), this.launcher)) {
break;
}
this.launcher.installer.installModpackFromPath(packPath);
break;
case ContentType.WORLD:
const worldPath = `${this.launcher.envPaths.cache}/downloaded/content/${await contentVersion.getVersionFileName()}`;
if (!checkAndDownload(await contentVersion.getVersionFileURL(), worldPath, await contentVersion.getVersionFileSHA1(), this.launcher)) {
break;
}
const saves = `${this.versionLaunchWorkDir}/saves`;
await ensureDir(saves);
compressing.zip.uncompress(worldPath, saves);
break;
case ContentType.DATA_PACK:
this.launcher.error("misc.unsupported", "version.install_datapack");
break;
default:
break;
}
}
}