nitro_core 0.32.1

Core functionality for launching Minecraft
Documentation
/// Client-specific launch functionality
mod client;
/// Configuration for launch settings
mod configuration;
/// Actual launching of the game process
mod process;
/// Server-specific launch functionality
mod server;

use std::fs::File;
use std::io::Write;
use std::ops::DerefMut;
use std::path::{Path, PathBuf};

use anyhow::{Context, bail};
use nitro_shared::java_args::MemoryArg;
use nitro_shared::output::{MessageContents, NitroOutput};
use nitro_shared::versions::VersionName;
use nitro_shared::{Side, translate};

use self::client::create_quick_play_args;
use self::process::{LaunchGameProcessParameters, launch_game_process};
use crate::account::AccountManager;
use crate::account::auth::check_game_ownership;
use crate::config::BrandingProperties;
use crate::instance::InstanceKind;
use crate::io::files::paths::Paths;
use crate::io::java::classpath::Classpath;
use crate::io::java::install::JavaInstallation;
use crate::net::game_files::client_meta::ClientMeta;
use crate::net::game_files::version_manifest::VersionManifestAndList;

pub use self::configuration::{
	LaunchConfigBuilder, LaunchConfiguration, QuickPlayType, WrapperCommand,
};

pub use self::process::get_stdio_file_path;
pub use self::process::launch_process;
pub use self::process::{LaunchProcessParameters, LaunchProcessProperties};

pub(crate) async fn launch(
	params: LaunchParameters<'_>,
	o: &mut impl NitroOutput,
) -> anyhow::Result<InstanceHandle> {
	let command = params.java.get_jvm_path();

	// Make sure we are authenticated
	if let InstanceKind::Client { .. } = &params.side {
		let mut process = o.get_process();
		let message = translate!(process, StartAuthenticating);
		process.display(MessageContents::StartProcess(message));

		// if !params.
		params
			.accounts
			.authenticate(
				params.offline_auth,
				params.paths,
				params.req_client,
				process.deref_mut(),
			)
			.await
			.context("Failed to ensure authentication")?;

		// Ensure game ownership in case we are using an alternative auth system
		let owns_game =
			check_game_ownership(params.paths).context("Failed to check for game ownership")?;

		if !owns_game {
			bail!(
				"Could not prove game ownership. If using an alternative auth system, like from a plugin, you must login with a Microsoft account that owns Minecraft first."
			);
		}

		let message = translate!(process, FinishAuthenticating);
		process.display(MessageContents::Success(message));
	}

	// Get side-specific launch properties
	let props = match params.side.get_side() {
		Side::Client => self::client::get_launch_props(&params).await,
		Side::Server => self::server::get_launch_props(&params),
	}
	.context("Failed to generate side-specific launch properties")?;

	let account_access_token = params
		.accounts
		.get_chosen_account()
		.and_then(|x| x.get_access_token());

	let proc_params = LaunchGameProcessParameters {
		command: command.as_os_str(),
		cwd: params.launch_dir,
		main_class: Some(params.main_class),
		classpath: params.classpath.clone(),
		paths: params.paths,
		props,
		launch_config: params.launch_config,
		version: params.version,
		version_list: &params.version_manifest.list,
		side: params.side,
		account_access_token,
		censor_secrets: params.censor_secrets,
		pipe_stdin: params.pipe_stdin,
	};

	launch_game_process(proc_params, o).context("Failed to launch game process")
}

/// Container struct for parameters for launching an instance
pub(crate) struct LaunchParameters<'a> {
	pub version: &'a VersionName,
	pub version_manifest: &'a VersionManifestAndList,
	pub side: &'a InstanceKind,
	pub launch_dir: &'a Path,
	pub java: &'a JavaInstallation,
	pub classpath: &'a Classpath,
	pub jar_path: &'a Path,
	pub main_class: &'a str,
	pub launch_config: &'a LaunchConfiguration,
	pub paths: &'a Paths,
	pub req_client: &'a reqwest::Client,
	pub client_meta: &'a ClientMeta,
	pub accounts: &'a mut AccountManager,
	pub offline_auth: bool,
	pub censor_secrets: bool,
	pub branding: &'a BrandingProperties,
	pub pipe_stdin: bool,
}

impl LaunchConfiguration {
	/// Create the args for the JVM when launching the game
	pub fn generate_jvm_args(&self) -> Vec<String> {
		let mut out = self.jvm_args.clone();

		if let Some(n) = &self.min_mem {
			out.push(MemoryArg::Min.to_string(n));
		}
		if let Some(n) = &self.max_mem {
			out.push(MemoryArg::Max.to_string(n));
		}

		out
	}

	/// Create the args for the game when launching
	pub fn generate_game_args(
		&self,
		version: &str,
		version_list: &[String],
		side: Side,
		o: &mut impl NitroOutput,
	) -> Vec<String> {
		let mut out = self.game_args.clone();

		out.extend(self.generate_additional_game_args(version, version_list, side, o));

		out
	}

	/// Create additional args for the game when launching
	pub fn generate_additional_game_args(
		&self,
		version: &str,
		version_list: &[String],
		side: Side,
		o: &mut impl NitroOutput,
	) -> Vec<String> {
		let mut out = Vec::new();

		if let Side::Client = side {
			out.extend(create_quick_play_args(
				&self.quick_play,
				version,
				version_list,
				o,
			));
		}

		out
	}
}

/// Handle for an instance after launching it. You must make sure to use
/// .wait() so that the child process is awaited.
#[derive(Debug)]
pub struct InstanceHandle {
	/// The child process for the launched instance
	process: std::process::Child,
	/// The stdout file of the process
	stdout: File,
	/// The stdout path of the process
	stdout_path: PathBuf,
	/// The stdin file of the process
	stdin: Option<File>,
	/// The stdin path of the process
	stdin_path: Option<PathBuf>,
	/// The classpath used to launch the instance
	classpath: Classpath,
}

impl InstanceHandle {
	/// Construct a new InstanceHandle
	fn new(
		process: std::process::Child,
		stdout: File,
		stdout_path: PathBuf,
		stdin: Option<File>,
		stdin_path: Option<PathBuf>,
		classpath: Classpath,
	) -> Self {
		Self {
			process,
			stdout,
			stdout_path,
			stdin,
			stdin_path,
			classpath,
		}
	}

	/// Waits for the process to complete
	pub fn wait(&mut self) -> std::io::Result<std::process::ExitStatus> {
		self.process.wait()
	}

	/// Kills the process early
	pub fn kill(&mut self) -> std::io::Result<()> {
		self.process.kill()
	}

	/// Returns the exit status of the instance process if it has finished
	pub fn try_wait(&mut self) -> std::io::Result<Option<std::process::ExitStatus>> {
		self.process.try_wait()
	}

	/// Gets the internal child process for the game, consuming the
	/// InstanceHandle and removing the stdio files
	pub fn get_process(self) -> std::process::Child {
		self.process
	}

	/// Gets the PID of the instance process
	pub fn get_pid(&self) -> u32 {
		self.process.id()
	}

	/// Gets the stdout file of the instance process
	pub fn stdout(&mut self) -> &mut File {
		&mut self.stdout
	}

	/// Gets the stdout path of the instance process
	pub fn stdout_path(&self) -> &Path {
		&self.stdout_path
	}

	/// Gets the stdin file of the instance process. Empty if it is piped.
	pub fn stdin(&mut self) -> Option<&mut File> {
		self.stdin.as_mut()
	}

	/// Gets the stdin path of the instance process. Empty if it is piped.
	pub fn stdin_path(&self) -> Option<&Path> {
		self.stdin_path.as_deref()
	}

	/// Writes to stdin
	pub fn write_stdin(&mut self, data: &[u8]) -> std::io::Result<()> {
		if let Some(stdin) = &mut self.stdin {
			stdin.write_all(data)
		} else {
			Ok(())
		}
	}

	/// Gets the classpath used to launch the instance
	pub fn classpath(&self) -> &Classpath {
		&self.classpath
	}
}