dmclc
Version:
Dolphin Minecraft Launcher Core
282 lines (281 loc) • 13.4 kB
JavaScript
import { execFileSync } from "child_process";
import compressing from "compressing";
import fs from "fs";
import fsextra from "fs-extra";
import got from "got";
import { ArtifactVersion, ComparableVersion, VersionRange } from "maven-artifact-version";
import StreamZip from "node-stream-zip";
import nodePath from "path";
import toml from "toml";
import { parseStringPromise } from "xml2js";
import { ModInfo } from "../../mods/mod.js";
import { merge } from "../../utils/MergeVersionJSONs.js";
import { checkFile } from "../../utils/check_file.js";
import { downloadIntoStream } from "../../utils/downloads.js";
import { expandMavenId } from "../../utils/maven.js";
import { transformJSON } from "../../utils/transformJSON.js";
import { ModLoadingIssue } from "../loader.js";
import * as fsPromises from "fs/promises";
import * as streamPromises from "stream/promises";
let temp = (await import("temp")).track();
export class ForgeLikeLoader {
launcher;
constructor(launcher) {
this.launcher = launcher;
}
async getSuitableLoaderVersions(MCVersion) {
if (MCVersion.extras.version === "Unknown") {
await this.launcher.error("loaders.minecraft_version_unknown");
}
const [, major, minor] = MCVersion.extras.version.split(".");
const majorn = parseInt(major);
const minorn = parseInt(minor);
if (majorn < 5 || (majorn === 5 && minorn != 2)) {
return [];
}
const res = await got(`${this.mavenArtifactURL}/${this.getArchiveBaseName(MCVersion.extras.version)}/maven-metadata.xml`);
const obj = await parseStringPromise(res.body);
const versions = obj.metadata.versioning[0].versions[0].version;
return versions.filter((v) => this.matchVersion(v, MCVersion.extras.version));
}
async install(MCVersion, version) {
if (MCVersion.extras.version === "Unknown") {
await this.launcher.error("loaders.minecraft_version_unknown");
return false;
}
let abn = this.getArchiveBaseName(MCVersion.extras.version);
const path = temp.createWriteStream({ prefix: `${abn}-${version}_installer`, suffix: ".jar" });
if (!await downloadIntoStream(`${this.mavenArtifactURL}/${abn}/${version}/${abn}-${version}-installer.jar`, path, this.launcher)) {
return false;
}
path.end();
const installerPath = await temp.mkdir(`${abn}_installer`);
await compressing.zip.uncompress(path.path, installerPath);
const metadata = JSON.parse(fs.readFileSync(`${installerPath}/install_profile.json`).toString());
if ("processors" in metadata) { // 1.13+
if ("MOJMAPS" in metadata.data) {
const id = metadata.data.MOJMAPS.client.substring(1, metadata.data.MOJMAPS.client.length - 1);
await MCVersion.completeLibraries([
{
downloads: {
artifact: Object.assign({
path: expandMavenId(id)
}, MCVersion.versionObject.downloads.client_mappings),
},
name: id,
},
]);
}
if (fs.existsSync(`${installerPath}/maven`))
await fsextra.copy(`${installerPath}/maven`, `${this.launcher.rootPath}/libraries`);
if (!await MCVersion.completeLibraries(metadata.libraries)) {
return false;
}
const target = MCVersion.versionObject;
const source = JSON.parse(fs.readFileSync(`${installerPath}/version.json`).toString());
const result = merge(target, source);
MCVersion.versionObject = result;
await MCVersion.completeLibraries(source.libraries);
for (const item of metadata.processors) {
if (item.args.includes("DOWNLOAD_MOJMAPS")) {
continue;
}
if (item.sides === undefined || item.sides.includes("client")) {
let res = false;
if ("outputs" in item) {
const outputs = item.outputs;
res = true;
for (const k in outputs) {
if (Object.prototype.hasOwnProperty.call(outputs, k)) {
const v = outputs[k];
res &&= await checkFile(this.transformArguments(k, installerPath, MCVersion, metadata), this.transformArguments(v, installerPath, MCVersion, metadata));
}
}
}
if (res)
continue;
const jar = `${this.launcher.rootPath}/libraries/${expandMavenId(item.jar)}`;
const args = ["-cp",
`${item.classpath.map((i) => {
return `${this.launcher.rootPath}/libraries/${expandMavenId(i)}`;
}).join(nodePath.delimiter)}${nodePath.delimiter}${jar}`,
await getMainClass(jar),
...item.args.map((v) => this.transformArguments(v, installerPath, MCVersion, metadata))];
execFileSync(this.launcher.usingJava, args);
}
}
fs.writeFileSync(`${this.launcher.rootPath}/versions/${MCVersion.name}/${MCVersion.name}.json`, JSON.stringify(result));
}
else if (this.supportsOld) { // 1.12-
const target = MCVersion.versionObject;
const source = metadata.versionInfo;
const result = merge(target, source);
MCVersion.versionObject = result;
fs.writeFileSync(`${this.launcher.rootPath}/versions/${MCVersion.name}/${MCVersion.name}.json`, JSON.stringify(result));
fsextra.copyFile(`${installerPath}/${metadata.install.filePath}`, `${this.launcher.rootPath}/libraries/${expandMavenId(metadata.install.path)}`);
}
fsPromises.rm(installerPath, { recursive: true, force: true });
return false;
}
transformArguments(arg, installerPath, MCVersion, metadata) {
return arg.replaceAll(/\{(.+?)\}/g, (v, a) => {
if (a === "SIDE")
return "client";
if (a === "MINECRAFT_JAR")
return `${MCVersion.versionRoot}/${MCVersion.name}.jar`;
if (a === "BINPATCH")
return `${installerPath}/data/client.lzma`;
return metadata.data[a].client;
}).replaceAll(/\[(.+?)\]/g, (v, a) => `${this.launcher.rootPath}/libraries/${expandMavenId(a)}`);
}
async findModInfos(path) {
const zip = new StreamZip.async({
file: path
});
const ret = [];
const jarJarEntry = await zip.entry("META-INF/jarjar/metadata.json");
if (jarJarEntry) {
const data = JSON.parse(transformJSON((await zip.entryData(jarJarEntry)).toString()));
for (const jarObj of data.jars) {
const file = temp.createWriteStream();
streamPromises.pipeline(await zip.stream(jarObj.path), file);
file.close();
ret.push(...await this.findModInfos(file.path));
}
}
const newEntry = await zip.entry("META-INF/mods.toml");
if (newEntry) {
const data = toml.parse((await zip.entryData(newEntry)).toString());
for (const i of data.mods) {
if (i.version === "${file.jarVersion}") {
i.version = await getVersion(zip);
}
if (data.dependencies)
ret.push(new ModInfo(this.name, {
info: i,
deps: data.dependencies[i.modId],
jar: data
}, this.launcher));
else
ret.push(new ModInfo(this.name, {
info: i,
jar: data
}, this.launcher));
}
}
if (!this.supportsOld)
return ret;
const oldEntry = await zip.entry("mcmod.info");
if (oldEntry) {
const data = JSON.parse(transformJSON((await zip.entryData(oldEntry)).toString()));
for (const i of data) {
ret.push(new ModInfo("forge", i, this.launcher));
}
}
await zip.close();
return ret;
}
checkMods(mods, mc, loader) {
const ret = [];
loader = loader.split("-").pop();
const modIdVersions = {
minecraft: mc,
forge: loader,
Forge: loader
};
for (const mod of mods) {
if ("info" in mod.data) {
if (!modIdVersions[mod.data.info.modId] || new ComparableVersion(modIdVersions[mod.data.info.modId]).compareTo(new ComparableVersion(mod.data.info.version)) < 0)
modIdVersions[mod.data.info.modId] = mod.data.info.version;
}
else {
if (!modIdVersions[mod.data.modid] || new ComparableVersion(modIdVersions[mod.data.modid]).compareTo(new ComparableVersion(mod.data.version ?? "999.999.999")) < 0)
modIdVersions[mod.data.modid] = mod.data.version ?? "";
}
}
for (const mod of mods) {
if ("info" in mod.data) {
if (mod.data.deps)
for (const dep of mod.data.deps) {
if (dep.mandatory) {
const range = VersionRange.createFromVersionSpec(dep.versionRange);
if (!(dep.modId in modIdVersions && range?.containsVersion(ArtifactVersion.of(modIdVersions[dep.modId])))) {
ret.push(new ModLoadingIssue("error", "dependencies.dependency_wrong_missing", {
source: mod.data.info.modId,
target: dep.modId,
targetVersion: dep.versionRange
}));
}
}
}
}
else if (parseInt(mc.split(".")[1]) <= 12 && this.supportsOld) {
if (mod.data.useDependencyInformation) {
if (mod.data.requiredMods)
for (const dep of mod.data.requiredMods) {
if (dep.includes("@")) {
const [depid, depver] = dep.split("@");
const range = VersionRange.createFromVersionSpec(depver);
if (!(depid in modIdVersions && range?.containsVersion(ArtifactVersion.of(modIdVersions[depid])))) {
ret.push(new ModLoadingIssue("error", "dependencies.dependency_wrong_missing", {
source: mod.data.modid,
target: depid,
targetVersion: depver
}));
}
}
else {
if (!(dep in modIdVersions)) {
ret.push(new ModLoadingIssue("error", "dependencies.dependency_wrong_missing", {
source: mod.data.modid,
target: dep,
targetVersion: this.launcher.i18n("misc.any")
}));
}
}
}
}
if (mod.data.mcversion && !(mod.data.mcversion === mc)) {
ret.push(new ModLoadingIssue("error", "dependencies.minecraft_wrong", {
source: mod.data.modid,
current: mc,
need: mod.data.mcversion
}));
}
}
}
return ret;
}
getModInfo(mod) {
if ("info" in mod) {
return {
id: mod.info.modId,
version: mod.info.version,
name: mod.info.displayName,
description: mod.info.description,
license: mod.jar.license,
isJIJLib: false
};
}
return {
id: mod.modid,
version: mod.version ?? "",
name: mod.name,
description: mod.description,
isJIJLib: false
};
}
}
async function getMainClass(jar) {
/* eslint-disable new-cap */
const zip = new StreamZip.async({
file: jar
});
const ret = (await zip.entryData("META-INF/MANIFEST.MF")).toString().split("\n").filter(i => i.startsWith("Main-Class:"))[0].replaceAll("Main-Class:", "").trim();
await zip.close();
return ret;
}
async function getVersion(jar) {
/* eslint-disable new-cap */
return (await jar.entryData("META-INF/MANIFEST.MF")).toString().split("\n").filter(i => i.startsWith("Implementation-Version:"))[0]?.replaceAll("Implementation-Version:", "").trim();
}