use std::path::{Path, PathBuf};
use crate::{
command::builder::{build_launch_command, LaunchCommand, LaunchOptions},
core::version::VersionJson,
install::{
client::{
fetch_vanilla_version, install_version_files, load_version_json, write_version_json,
},
loader::{run_loader_installer, write_loader_profile, InstallerInvocation},
request::{InstallRequest, InstallResult},
},
loader::{
common::{LoaderSpec, LoaderVersion},
LoaderKind,
},
net::download::{execute_plan, DownloadPlan, DownloadTask},
progress::{ProgressEvent, ProgressReporter},
LauncherError, Result,
};
#[derive(Debug, Clone)]
pub struct Launcher {
minecraft_dir: PathBuf,
}
impl Launcher {
pub fn new(minecraft_dir: impl Into<PathBuf>) -> Self {
Self {
minecraft_dir: minecraft_dir.into(),
}
}
pub fn minecraft_dir(&self) -> &Path {
&self.minecraft_dir
}
pub fn install(&self, request: InstallRequest) -> Result<InstallResult> {
let mut reporter = |_event: ProgressEvent| {};
self.install_with_progress(request, &mut reporter)
}
pub fn install_with_progress(
&self,
request: InstallRequest,
reporter: &mut dyn ProgressReporter,
) -> Result<InstallResult> {
if let Some(loader) = request.loader.clone() {
match loader {
LoaderSpec::Fabric { version } => {
self.install_vanilla_version(&request.minecraft_version, reporter)?;
let loader_version = resolve_fabric_loader_version(version)?;
let profile = crate::loader::fabric::fetch_profile(
&request.minecraft_version,
&loader_version,
)?;
let version_id = version_id(&profile, "loader profile")?.to_string();
write_loader_profile(&self.minecraft_dir, &profile)?;
let merged = self.load_version(&version_id)?;
install_version_files(&merged, &self.minecraft_dir, reporter)?;
return Ok(InstallResult { version_id });
}
LoaderSpec::Quilt { version } => {
self.install_vanilla_version(&request.minecraft_version, reporter)?;
let loader_version = resolve_quilt_loader_version(version)?;
let profile = crate::loader::quilt::fetch_profile(
&request.minecraft_version,
&loader_version,
)?;
let version_id = version_id(&profile, "loader profile")?.to_string();
write_loader_profile(&self.minecraft_dir, &profile)?;
let merged = self.load_version(&version_id)?;
install_version_files(&merged, &self.minecraft_dir, reporter)?;
return Ok(InstallResult { version_id });
}
LoaderSpec::Forge { version } => {
let loader_version = resolve_forge_loader_version(version)?;
let installer_path = download_installer(
&self.minecraft_dir,
"forge",
&loader_version,
&crate::loader::forge::installer_url(&loader_version),
)?;
run_loader_installer(&InstallerInvocation {
loader: LoaderKind::Forge,
java_executable: PathBuf::from("java"),
installer_path,
minecraft_dir: self.minecraft_dir.clone(),
})?;
return Ok(InstallResult {
version_id: crate::loader::forge::forge_installed_version_id(
&loader_version,
)?,
});
}
LoaderSpec::NeoForge { version } => {
let loader_version = resolve_neoforge_loader_version(version)?;
let installer_path = download_installer(
&self.minecraft_dir,
"neoforge",
&loader_version,
&crate::loader::neoforge::installer_url(&loader_version),
)?;
run_loader_installer(&InstallerInvocation {
loader: LoaderKind::NeoForge,
java_executable: PathBuf::from("java"),
installer_path,
minecraft_dir: self.minecraft_dir.clone(),
})?;
return Ok(InstallResult {
version_id: crate::loader::neoforge::neoforge_installed_version_id(
&request.minecraft_version,
&loader_version,
),
});
}
}
}
self.install_vanilla_version(&request.minecraft_version, reporter)?;
Ok(InstallResult {
version_id: request.minecraft_version,
})
}
pub fn build_launch_command_from_version(
&self,
version: &VersionJson,
options: LaunchOptions,
) -> Result<LaunchCommand> {
build_launch_command(version, self.minecraft_dir.clone(), options)
}
pub fn load_version(&self, version_id: &str) -> Result<VersionJson> {
load_version_json(&self.minecraft_dir, version_id)
}
fn install_vanilla_version(
&self,
version_id: &str,
reporter: &mut dyn ProgressReporter,
) -> Result<()> {
let version = fetch_vanilla_version(version_id)?;
write_version_json(&self.minecraft_dir, &version)?;
install_version_files(&version, &self.minecraft_dir, reporter)
}
}
fn version_id<'a>(version: &'a VersionJson, context: &str) -> Result<&'a str> {
version
.id
.as_deref()
.ok_or_else(|| LauncherError::MissingField {
context: context.to_string(),
field: "id".to_string(),
})
}
fn resolve_fabric_loader_version(version: LoaderVersion) -> Result<String> {
match version {
LoaderVersion::Exact(version) => Ok(version),
LoaderVersion::Latest | LoaderVersion::LatestStable => {
let versions = crate::loader::fabric::list_loader_versions()?;
Ok(crate::loader::fabric::latest_stable_loader(&versions)?
.version
.clone())
}
}
}
fn resolve_quilt_loader_version(version: LoaderVersion) -> Result<String> {
match version {
LoaderVersion::Exact(version) => Ok(version),
LoaderVersion::Latest | LoaderVersion::LatestStable => {
let versions = crate::loader::quilt::list_loader_versions()?;
Ok(crate::loader::quilt::latest_loader(&versions)?
.version
.clone())
}
}
}
fn resolve_forge_loader_version(version: LoaderVersion) -> Result<String> {
match version {
LoaderVersion::Exact(version) => Ok(version),
LoaderVersion::Latest | LoaderVersion::LatestStable => {
let versions = crate::loader::forge::list_forge_versions()?;
versions
.last()
.cloned()
.ok_or_else(|| LauncherError::LoaderVersionNotFound {
loader: LoaderKind::Forge,
version: "latest".to_string(),
})
}
}
}
fn resolve_neoforge_loader_version(version: LoaderVersion) -> Result<String> {
match version {
LoaderVersion::Exact(version) => Ok(version),
LoaderVersion::Latest | LoaderVersion::LatestStable => {
let versions = crate::loader::neoforge::list_neoforge_versions()?;
versions
.last()
.cloned()
.ok_or_else(|| LauncherError::LoaderVersionNotFound {
loader: LoaderKind::NeoForge,
version: "latest".to_string(),
})
}
}
}
fn download_installer(
minecraft_dir: &Path,
loader_name: &str,
loader_version: &str,
url: &str,
) -> Result<PathBuf> {
let destination = minecraft_dir
.join("versions")
.join(".installers")
.join(format!("{loader_name}-{loader_version}-installer.jar"));
let plan = DownloadPlan {
tasks: vec![DownloadTask {
url: url.to_string(),
destination: destination.clone(),
checksum: None,
label: format!("{loader_name} installer {loader_version}"),
}],
};
let mut reporter = |_event: ProgressEvent| {};
execute_plan(&plan, &mut reporter)?;
Ok(destination)
}