dmclc
Version:
Dolphin Minecraft Launcher Core
271 lines (270 loc) • 11 kB
JavaScript
import compressing from "compressing";
import fs from "fs";
import { mkdirsSync, remove } from "fs-extra";
import * as i18next from "i18next";
import FsBackend from "i18next-fs-backend";
import { marked } from "marked";
import os, { homedir } from "os";
import { pathToFileURL } from "url";
import { AuthlibInjectorAccount } from "./auth/ali_account.js";
import { MicrosoftAccount } from "./auth/microsoft/microsoft_account.js";
import { MinecraftUniversalLoginAccount } from "./auth/mul_account.js";
import { OfflineAccount } from "./auth/offline_account.js";
import { FormattedError } from "./errors/FormattedError.js";
import { Installer } from "./install.js";
import { FabricLoader } from "./loaders/fabric.js";
import { VersionParser } from "./loaders/fabriclike/version/VersionParser.js";
import { ForgeLoader } from './loaders/forge.js';
import { NeoForgeLoader } from "./loaders/neoforge.js";
import { QuiltLoader } from "./loaders/quilt/quilt.js";
import CurseForgeContentService from "./mods/download/curseforge/CurseForgeContentService.js";
import ModrinthContentService from "./mods/download/modrinth/ModrinthContentService.js";
import { CurseForgeModpackFormat } from "./mods/modpack/curseforge/CurseForgeModpack.js";
import { ModrinthModpackFormat } from "./mods/modpack/modrinth/ModrinthModpack.js";
import { checkAndDownload, download, downloadIntoStream } from "./utils/downloads.js";
import { MinecraftVersion } from "./version.js";
import envPaths from "env-paths";
import * as fsPromise from "fs/promises";
let temp = (await import("temp")).track();
class LocalizedProgress {
dest;
t;
constructor(dest, t) {
this.dest = dest;
this.t = t;
}
update(msg) {
this.dest.update(this.t(msg));
}
close() {
this.dest.close();
}
}
/**
* The core of DMCLC.
* @public
*/
class Launcher {
name;
clientId;
launcherInterface;
downloader;
copy;
/** @see os.platform */
systemType = os.platform();
natives;
/** BMCLAPI */
mirror;
installer = new Installer(this);
/** All loaders. */
loaders = new Map();
/** All account types. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
accountTypes = new Map();
contentServices = new Map();
modpackFormats = new Map();
/** Using Java executable */
usingJava;
/** All installed versions. */
installedVersions = new Map();
i18n = i18next.t;
archInfo;
envPaths = envPaths("DMCLC");
realRootPath = "";
static version = "4.4.0-beta.2";
/**
* Create a new Launcher object.
* @throws {@link FormattedError}
* @param rootPath - path to .minecraft
* @param name - Launcher name.
* @param javaExec - {@link Launcher.usingJava}
*/
constructor(rootPath, name, javaExec, clientId, launcherInterface, downloader, copy) {
this.name = name;
this.clientId = clientId;
this.launcherInterface = launcherInterface;
this.downloader = downloader;
this.copy = copy;
this.realRootPath = fs.realpathSync(rootPath);
this.usingJava = javaExec;
if (this.systemType === "win32") {
this.natives = "windows";
}
else {
if (this.systemType === "linux") {
this.natives = "linux";
}
else if (this.systemType === "darwin") {
this.natives = "osx";
}
else {
throw new FormattedError("Unsupported platform.");
}
}
this.loaders.set("fabric", new FabricLoader(this));
this.loaders.set("quilt", new QuiltLoader(this));
this.loaders.set("forge", new ForgeLoader(this));
this.loaders.set("neoforge", new NeoForgeLoader(this));
this.accountTypes.set("microsoft", (data) => new MicrosoftAccount(data, this));
this.accountTypes.set("offline", (data) => new OfflineAccount(data, this));
this.accountTypes.set("authlib_injector", (data) => new AuthlibInjectorAccount(data, this));
this.accountTypes.set("minecraft_universal_login", (data) => new MinecraftUniversalLoginAccount(data, this));
this.contentServices.set("modrinth", new ModrinthContentService(this));
this.contentServices.set("curseforge", new CurseForgeContentService(this));
this.modpackFormats.set("modrinth", new ModrinthModpackFormat());
this.modpackFormats.set("curseforge", new CurseForgeModpackFormat());
this.refreshInstalledVersion();
}
/**
* Create a new Launcher object.
* @throws {@link FormattedError}
* @throws RequestError
* @param rootPath - path to .minecraft
* @param name - Launcher name.
* @param javaExec - {@link Launcher.usingJava}
* @param clientId - Microsoft identify platform APP id.
* @param downloader - Custom downloading function.
* @param copy - Custom clipboard function.
* @returns Launcher.
*/
static async create(rootPath, name, javaExec, clientId, launcherInterface, lang = "en_us", downloader, copy) {
const launcher = new Launcher(rootPath, name, javaExec, clientId, launcherInterface, downloader, copy);
await launcher.init(lang);
return launcher;
}
async init(lang) {
if (fs.existsSync(`${homedir()}/.dmclc`)) {
await fsPromise.rm(`${homedir()}/.dmclc`, { recursive: true, force: true });
}
if (os.platform() === "linux") {
// Special thanks to HMCL. Sorry for I'm not able to check if this works properly.
if (process.arch !== "x64" && process.arch !== "ia32") {
await checkAndDownload("https://raw.githubusercontent.com/huanghongxun/HMCL/javafx/HMCL/src/main/resources/assets/natives.json", `${this.envPaths.cache}/natives.json`, "", this);
const specialNatives = JSON.parse((await fs.promises.readFile(`${this.envPaths.cache}/natives.json`)).toString())[this.getArchString()];
this.archInfo = {
specialArch: process.arch,
specialNatives
};
}
}
if (!fs.existsSync(`${this.envPaths.cache}/locales`)
|| VersionParser.parseSemantic((await fs.promises.readFile(`${this.envPaths.cache}/locales/version`)).toString().trim())
.compareTo(VersionParser.parseSemantic(Launcher.version)) < 0) {
await download("https://heipiao233.github.io/dmclc/locales.tar.gz", `${this.envPaths.cache}/locales.tar.gz`, this);
await compressing.tgz.uncompress(`${this.envPaths.cache}/locales.tar.gz`, this.envPaths.cache);
}
this.i18n = await i18next.use(FsBackend).init({
lng: lang,
backend: {
loadPath: `${this.envPaths.cache}/locales/{{lng}}.json`
}
});
}
/**
* Refresh installed versions.
*/
refreshInstalledVersion() {
this.installedVersions.clear();
if (!fs.existsSync(`${this.rootPath}/versions`)) {
mkdirsSync(`${this.rootPath}/versions`);
this.installedVersions.clear();
return;
}
fs.readdirSync(`${this.rootPath}/versions`)
.filter(value => fs.existsSync(`${this.rootPath}/versions/${value}/${value}.json`))
.forEach(name => this.installedVersions.set(name, MinecraftVersion.fromVersionName(this, name)));
}
/**
* The path to the ".minecraft" directory.
*/
get rootPath() {
return this.realRootPath;
}
set rootPath(path) {
this.realRootPath = fs.realpathSync(path);
this.refreshInstalledVersion();
}
getArchString() {
let arch;
switch (os.arch()) {
case "arm":
arch = "arm32";
break;
case "arm64" || "aarch64":
arch = "arm64";
break;
case "mips64el":
arch = "mips64el";
break;
case "loongarch64":
if (VersionParser.parse(os.release(), false).compareTo(VersionParser.parse("5.19", false)) <= 0) {
arch = "loongarch64_ow";
}
else
arch = "loongarch64";
break;
default:
arch = os.arch();
break;
}
return `${this.natives}-${arch}`;
}
async removeVersion(version) {
await remove(version.versionRoot);
this.refreshInstalledVersion();
}
async askUser(questions, message) {
return await this.launcherInterface.askUser(questions, message);
}
async askUserOne(localizeKey, message) {
return await this.launcherInterface.askUserOne(this.i18n(localizeKey), message);
}
async info(message, title = "misc.info") {
await this.launcherInterface.info(this.i18n(message), this.i18n(title));
}
async warn(message, title = "misc.warn") {
await this.launcherInterface.warn(this.i18n(message), this.i18n(title));
}
async error(message, title = "misc.error") {
await this.launcherInterface.error(this.i18n(message), this.i18n(title));
}
createProgress(steps, title, msg) {
return new LocalizedProgress(this.launcherInterface.createProgress(steps, this.i18n(title), this.i18n(msg)), this.i18n);
}
setDownloadImages() {
let self = this;
marked.setOptions({
async walkTokens(token) {
let url = "";
try {
if (token.type == "image") {
url = token.href;
let file = temp.createWriteStream();
await downloadIntoStream(url, file, self);
file.close();
token.href = pathToFileURL(file.path).toString();
}
else if (token.type == "html") {
if (!token.raw.includes("<img"))
return;
let m = token.raw.match(/src=\"(.+?)\"/);
if (!m || m.length < 2)
return;
url = m[1];
let file = temp.createWriteStream();
await downloadIntoStream(url, file, self);
file.close();
token.raw = token.raw.replaceAll(url, pathToFileURL(file.path).toString());
token.text = token.text.replaceAll(url, pathToFileURL(file.path).toString());
}
}
catch {
self.launcherInterface.error(self.i18n("content_service.image_load_fail_detail", { url }), "content_service.image_load_fail_title");
}
},
gfm: true,
async: true
});
}
}
export { Launcher };