UNPKG

@origami-minecraft/devbuilds

Version:

Origami is a terminal-first Minecraft launcher that supports authentication, installation, and launching of Minecraft versions โ€” with built-in support for Microsoft accounts, mod loaders, profile management, and more. Designed for power users, modders, an

485 lines (392 loc) โ€ข 17.7 kB
import { Credentials, IAuthProvider } from "../../../types/account"; import { LauncherAccount, LauncherProfile } from "../../../types/launcher"; import LauncherProfileManager from "../../tools/launcher"; import { ensureDir, minecraft_dir, printVersion } from "../../utils/common"; import { getAuthProvider } from "../account"; import LauncherAccountManager from "../account/account"; import path from "path"; import temurin from "../../../java"; import parseArgsStringToArgv from "string-argv"; import LauncherOptionsManager from "./options"; import { Logger } from "../../tools/logger"; import { ORIGAMI_CLIENT_TOKEN } from "../../../config/defaults"; import chalk from "chalk"; import MCLCore from "../../launcher"; import { ILauncherOptions, IUser } from "../../launcher/types"; import { InstallerRegistry } from "../install/registry"; import inquirer from "inquirer"; import { existsSync, writeFileSync, readFileSync, remove } from "fs-extra"; import ModrinthModManager from "../install/packs/manager"; export const logger = new Logger(); export const progress = logger.progress(); export class Handler { public profiles: LauncherProfileManager = new LauncherProfileManager(); public accounts: LauncherAccountManager = new LauncherAccountManager(); public settings: LauncherOptionsManager = new LauncherOptionsManager(); public installers: InstallerRegistry = new InstallerRegistry(); private auth_provider: IAuthProvider | null = null; private currentAccount: LauncherAccount | null = null; constructor() {} private jsonParser(str: string) { try { return JSON.parse(str); } catch(_) { return {}; } } private launcherToUser(la: LauncherAccount): IUser { return { access_token: la.access_token, client_token: la.client_token || ORIGAMI_CLIENT_TOKEN, uuid: la.uuid, name: la.name ?? "Origami-User", user_properties: typeof la.user_properties === "string" ? this.jsonParser(la.user_properties) : la.user_properties ?? {}, meta: la.meta ? { type: la.meta.type === "msa" ? "msa" : "mojang", demo: la.meta.demo, } : undefined, }; } private getVersion(versionJson: string) { let version_data = this.jsonParser(readFileSync(versionJson, { encoding: "utf-8" })); return { version: version_data["inheritsFrom"] || version_data["id"], type: version_data["type"] || "release", }; } public async get_auth(): Promise<{ jvm: string; token: LauncherAccount } | null> { this.currentAccount = await this.accounts.getSelectedAccount(); if (!this.currentAccount) { logger.warn("โš ๏ธ No account selected! Please log in first. ๐Ÿพ"); return null; } const auth = await getAuthProvider(this.currentAccount); if (!auth) { logger.warn("โŒ Failed to load the appropriate auth provider."); return null; } this.auth_provider = auth; const jvmArgs = await auth.auth_lib() ?? ""; logger.log("๐Ÿ” Authenticating... Please wait."); const token = await auth.token(); if (!token) { logger.warn("๐Ÿšซ Authentication token could not be retrieved."); return null; } return { jvm: jvmArgs, token, }; } public async login(credentials: Credentials, auth_provider: string): Promise<LauncherAccount | null> { try { if (await this.accounts.hasAccount(credentials, auth_provider)) { logger.warn("โš ๏ธ An account with these credentials already exists! Skipping login. ๐Ÿพ"); return null; } const auth = await getAuthProvider(auth_provider); if (!auth) { logger.error(`โŒ Could not resolve the auth provider: '${auth_provider}'`); return null; } this.auth_provider = auth; auth.set_credentials(credentials.email, credentials.password); logger.log("๐Ÿ” Authenticating... Please wait."); const token = await auth.authenticate(); if (!token) { logger.error("๐Ÿšซ Authentication failed โ€” invalid credentials or network error."); return null; } await this.accounts.addAccount(token); await this.accounts.selectAccount(token.id); this.currentAccount = token; logger.log(`โœ… Logged in as ${token.name} [${token.uuid}] via "${auth_provider}" ๐ŸŽ‰`); return token; } catch (err) { logger.error("๐Ÿ’ฅ Unexpected error during login:", (err as Error).message); return null; } } public async choose_profile() { return this.profiles.chooseProfile(); } public async choose_account() { return this.accounts.chooseAccount(); } public async run_minecraft(_name?: string): Promise<200 | null> { let mc_dir = minecraft_dir(); let version_dir = path.join(mc_dir, 'versions'); let cache_dir = path.join(mc_dir, '.cache'); let selected_profile = this.profiles.getSelectedProfile(); let name = selected_profile?.name || _name; if (!_name && (!selected_profile || !name) || !name) { console.log(chalk.bgHex('#f87171').hex('#fff')(' ๐Ÿ’” No profile selected! ') + chalk.hex('#fca5a5')('Please pick a profile before launching the game.')); return null; } if(selected_profile) this.settings.setProfile(selected_profile) else this.settings.setProfile(); let version_path = path.join(version_dir, name); let version_json = path.join(version_path, `${name}.json`); if(!existsSync(version_path) || !existsSync(version_json)) { return null; } let origami_dir = minecraft_dir(true); let origami_data = path.join(origami_dir, 'instances', name); ensureDir(origami_data); try { let java = await temurin.select(false, selected_profile?.origami.version); let auth = await this.get_auth(); if(!java || !auth) return null; return await new Promise(async(resolve) => { let libraryRoot = path.join(mc_dir, 'libraries') let assetRoot = path.join(mc_dir, 'assets'); let loader = this.installers.get(this.installers.list().sort((a, b) => b.length - a.length).find(ld => name.toLowerCase().startsWith(ld) || name.toLowerCase().includes(ld)) || 'vanilla'); let jvmArgs = `${auth.jvm}`; let additional_jvm = this.profiles.getJvm(selected_profile?.origami.version || ''); if (additional_jvm !== '') { logger.log(`โš ๏ธ Additional JVM Flags: ${additional_jvm}`); jvmArgs = `${jvmArgs} ${additional_jvm}` } if (loader && loader.metadata.unstable) { logger.warn(`โš ๏ธ Heads up! ${loader.metadata.name} support is a bit wobbly right now โ€” it might break or misbehave ๐Ÿงช๐Ÿ‘€`); } if (loader && loader.metadata.jvm) { jvmArgs = `${loader.metadata.jvm} ${jvmArgs}`; } let javaPath = java.path; if (selected_profile) { const mod_manager = new ModrinthModManager(selected_profile); const installed = mod_manager.getList().mods; if (installed.length === 0) { logger.log(chalk.yellow('โœจ No mods installed for this profile.')); } else { logger.log(chalk.green.bold('\n๐Ÿ“ฆ Installed:\n')); installed.forEach((mod, index) => { let isDisabled = mod_manager.isModDisabled(mod); logger.log(chalk.cyan(` ${isDisabled ? `${chalk.gray('(disabled)')} ` : ''}${index + 1}. ${mod}`)); }); } } let auth_token = auth.token; let version = this.getVersion(version_json); let settings = this.settings.getFixedOptions(); let memory = settings.memory; let window = settings.window_size; let metadata = { name: 'Origami', version: printVersion(), }; let launcher = new MCLCore(); let instance: ILauncherOptions = { authorization: this.launcherToUser(auth_token), root: mc_dir, customArgs: parseArgsStringToArgv(jvmArgs), version: { number: version.version, type: version.type, custom: name, }, memory, javaPath, window: { ...window, fullscreen: settings.fullscreen, }, cache: cache_dir, overrides: { libraryRoot, assetRoot, gameDirectory: origami_data, cwd: version_path, detached: settings.safe_exit, maxSockets: settings.max_sockets, connections: settings.connections, versionName: `${metadata.name}/${metadata.version}`, }, launcher: metadata, }; launcher.launch(instance).then((proc) => { logger.warn(`Minecraft PID: ${chalk.yellow(proc?.pid || "<cannot be fetched>")}`) }); launcher.on("close", () => { resolve(200); }); launcher.on('debug', (e) => logger.log(chalk.grey(String(e)).trim())); launcher.on('minecraft-log', (e) => logger.log(chalk.white(String(e)).trim())); launcher.on('minecraft-error', (e) => logger.log(chalk.redBright(String(e)).trim())); launcher.on('progress', (data) => { let { type, task, total } = data; if(!progress.has(type)) { progress.create(type, total); progress.start(); } progress.updateTo(type, task); }); launcher.on('progress-end', (data) => { if(progress.has(data.type)) { progress.stop(data.type); } }); launcher.on('download-status', (data) => { let { name, current, total } = data; if(!progress.has(name)) { progress.create(name, total, true); progress.start(); } progress.updateTo(name, current); }); launcher.on('download', (name) => { if(progress.has(name)) { progress.stop(name); } }); }) } catch(_) { return null; } } public configure_settings(): Promise<void> { return this.settings.configureOptions(); } public async install_version(): Promise<void> { const installers = this.installers; const availableInstallers = installers.list() .map(id => installers.get(id)) .filter((v) => v); if (availableInstallers.length === 0) { logger.warn("โš ๏ธ No available installers found."); return; } const minecraft_launcher_profiles = path.join(minecraft_dir(), 'launcher_profiles.json'); if(!existsSync(minecraft_launcher_profiles)) { writeFileSync(minecraft_launcher_profiles, JSON.stringify({ profiles: {} })); } const choices = availableInstallers.map(installer => ({ name: (installer?.metadata.unstable ? chalk.redBright('[UNSTABLE] ') : '') + chalk.green(`[${installer?.metadata.author}] `) + chalk.hex("#c4b5fd")(installer?.metadata.name) + chalk.magenta(" - " + chalk.yellow(installer?.metadata.description)), value: installer })); const defaultInstaller = availableInstallers.find(v => v?.metadata.name.toLowerCase() === "vanilla"); const { selected } = await inquirer.prompt([ { type: "list", name: "selected", message: chalk.hex("#f472b6")("๐ŸŒท Choose a version type to install:"), choices, default: defaultInstaller } ]); const selectedInstaller = selected; if (!selectedInstaller) { logger.error(`โŒ Installer "${selected}" not found.`); return; } logger.log(`๐Ÿ”ง Installing via ${selectedInstaller.metadata.name}...`); const result = await selectedInstaller.get(); if (result) { logger.success(`๐ŸŽ‰ Installed ${result.name} ${result.version} successfully!`); } else { logger.error(`โŒ Installation failed.`); } } public async remove_account(): Promise<void> { const accounts = await this.accounts.listAccounts(); if (accounts.length === 0) { logger.warn("โš ๏ธ No accounts available to remove."); return; } const { selected } = await inquirer.prompt([ { type: 'list', name: 'selected', message: chalk.hex('#f87171')('โŒ Select an account to remove:'), choices: accounts.map(acc => ({ name: `${acc.name} (${acc.uuid})`, value: acc.id })) } ]); const selectedAccount = accounts.find(acc => acc.id === selected); if (!selectedAccount) { logger.error("โŒ Selected account not found."); return; } const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: chalk.red(`Are you sure you want to delete account "${selectedAccount.name}"?`), default: false } ]); if (!confirm) { logger.log("๐Ÿ”™ Account removal cancelled."); return; } const removed = await this.accounts.deleteAccount(selected); if (removed) { logger.success(`๐Ÿ—‘๏ธ Removed account "${selectedAccount.name}" successfully!`); const selected = await this.accounts.getSelectedAccount(); if (!selected) { this.currentAccount = null; logger.warn("โš ๏ธ No account is now selected."); } else { this.currentAccount = selected; } } else { logger.error("โŒ Failed to remove the account."); } } public async delete_profile(): Promise<void> { const profiles = this.profiles.listProfiles().map(id => this.profiles.getProfile(id)).filter(v => typeof v !== 'undefined'); if (profiles.length === 0) { logger.warn("โš ๏ธ No profiles to delete."); return; } const { selected } = await inquirer.prompt([ { type: "list", name: "selected", message: chalk.hex("#f87171")("๐Ÿ—‘๏ธ Select a profile/instance to delete:"), choices: profiles.map(p => ({ name: `${p.name} (${p.origami.version || "unknown"})`, value: p })) } ]); const profile = selected as LauncherProfile; const { confirm } = await inquirer.prompt([ { type: "confirm", name: "confirm", message: chalk.red(`Are you sure you want to delete the "${profile.name}" profile and all associated data?`), default: false } ]); if (!confirm) { logger.log("โŒ Deletion cancelled."); return; } try { const mc_dir = minecraft_dir(); const origami_dir = minecraft_dir(true); const version_path = path.join(mc_dir, "versions", profile.origami.path); const instance_path = path.join(origami_dir, "instances", profile.origami.path); if (existsSync(version_path)) await remove(version_path); if (existsSync(instance_path)) await remove(instance_path); this.profiles.deleteProfile(profile.origami.version); logger.success(`๐Ÿ—‘๏ธ Successfully deleted profile "${profile.name}" and its data.`); } catch (err) { logger.error(`๐Ÿ’ฅ Failed to delete profile "${profile.name}":`, (err as Error).message); } } }