use std::path::{Path, PathBuf};
use anyhow::{bail, Context};
use mcvm_shared::output::MCVMOutput;
use mcvm_shared::Side;
use crate::config::BrandingProperties;
use crate::io::files::paths::Paths;
use crate::io::files::update_hardlink;
use crate::io::java::classpath::Classpath;
use crate::io::java::install::{JavaInstallParameters, JavaInstallation};
use crate::io::persistent::PersistentData;
use crate::io::update::UpdateManager;
use crate::launch::{LaunchConfiguration, LaunchParameters};
use crate::net::game_files::client_meta::ClientMeta;
use crate::net::game_files::version_manifest::VersionManifestAndList;
use crate::net::game_files::{game_jar, libraries};
use crate::user::UserManager;
use crate::util::versions::VersionName;
use crate::version::{ClientAssetsAndLibraries, ClientAssetsAndLibsParameters};
use crate::InstanceHandle;
pub const DEFAULT_SERVER_MAIN_CLASS: &str = "net.minecraft.server.Main";
pub struct Instance<'params> {
params: InstanceParameters<'params>,
config: InstanceConfiguration,
java: JavaInstallation,
jar_path: PathBuf,
classpath: Classpath,
main_class: String,
}
impl<'params> Instance<'params> {
pub(crate) async fn load(
config: InstanceConfiguration,
params: InstanceParameters<'params>,
o: &mut impl MCVMOutput,
) -> anyhow::Result<Instance<'params>> {
tokio::fs::create_dir_all(&config.path)
.await
.context("Failed to create instance directory")?;
if !config.path.is_dir() {
bail!("Instance directory path is not a directory");
}
let java_vers = ¶ms.client_meta.java_info.major_version;
let java_params = JavaInstallParameters {
paths: params.paths,
update_manager: params.update_manager,
persistent: params.persistent,
req_client: params.req_client,
};
let java =
JavaInstallation::install(config.launch.java.clone(), *java_vers, java_params, o)
.await
.context("Failed to install or update Java")?;
let is_valid = java
.verify()
.context("Failed to verify Java installation")?;
if !is_valid {
bail!("Java installation is invalid");
}
params.persistent.dump(params.paths).await?;
let mut jar_path = if let Some(jar_path) = &config.jar_path {
jar_path.clone()
} else {
game_jar::get(
config.side.get_side(),
params.client_meta,
params.version,
params.paths,
params.update_manager,
params.req_client,
o,
)
.await
.context("Failed to get the game JAR file")?;
crate::io::minecraft::game_jar::get_path(
config.side.get_side(),
params.version,
None,
params.paths,
)
};
if !jar_path.exists() {
bail!("Game JAR does not exist");
}
if let Side::Server = config.side.get_side() {
let new_jar_path = config.path.join("server.jar");
if new_jar_path != jar_path {
if params.update_manager.should_update_file(&new_jar_path) {
if new_jar_path.exists() {
tokio::fs::remove_file(&new_jar_path)
.await
.context("Failed to remove existing server.jar")?;
}
if params.disable_hardlinks {
tokio::fs::copy(&jar_path, &new_jar_path)
.await
.context("Failed to copy server.jar")?;
} else {
update_hardlink(&jar_path, &new_jar_path)
.context("Failed to hardlink server.jar")?;
}
params.update_manager.add_file(new_jar_path.clone());
}
}
jar_path = new_jar_path;
if !jar_path.exists() {
bail!("Game JAR does not exist");
}
}
if let Side::Client = config.side.get_side() {
let sub_params = ClientAssetsAndLibsParameters {
client_meta: params.client_meta,
version: params.version,
paths: params.paths,
req_client: params.req_client,
version_manifest: params.version_manifest,
update_manager: params.update_manager,
};
params
.client_assets_and_libs
.load(sub_params, o)
.await
.context("Failed to load client assets and libraries")?;
}
let mut classpath = Classpath::new();
if let Side::Client = config.side.get_side() {
let lib_classpath = libraries::get_classpath(params.client_meta, params.paths)
.context("Failed to extract classpath from game library list")?;
classpath.extend(lib_classpath);
}
for lib in &config.additional_libs {
classpath.add_path(lib)?;
}
classpath.add_path(&jar_path)?;
let main_class = if let Some(main_class) = &config.main_class {
main_class.clone()
} else {
match config.side.get_side() {
Side::Client => params.client_meta.main_class.clone(),
Side::Server => DEFAULT_SERVER_MAIN_CLASS.into(),
}
};
if let InstanceKind::Server { create_eula, .. } = &config.side {
if *create_eula {
let eula_path = config.path.join("eula.txt");
if !eula_path.exists() {
tokio::fs::write(eula_path, "eula = true\n")
.await
.context("Failed to create eula.txt")?;
}
}
}
Ok(Self {
config,
params,
java,
jar_path,
classpath,
main_class,
})
}
pub async fn launch(&mut self, o: &mut impl MCVMOutput) -> anyhow::Result<()> {
let mut handle = self.launch_with_handle(o).await?;
handle
.wait()
.context("Failed to wait for instance process")?;
Ok(())
}
pub async fn launch_with_handle(
&mut self,
o: &mut impl MCVMOutput,
) -> anyhow::Result<InstanceHandle> {
let params = LaunchParameters {
version: self.params.version,
version_manifest: self.params.version_manifest,
side: &self.config.side,
launch_dir: &self.config.path,
java: &self.java,
classpath: &self.classpath,
main_class: &self.main_class,
launch_config: &self.config.launch,
paths: self.params.paths,
req_client: self.params.req_client,
client_meta: self.params.client_meta,
users: self.params.users,
censor_secrets: self.params.censor_secrets,
branding: self.params.branding,
};
let handle = crate::launch::launch(params, o)
.await
.context("Failed to run launch routine")?;
Ok(handle)
}
pub fn get_jar_path(&self) -> &Path {
&self.jar_path
}
}
pub struct InstanceConfiguration {
pub side: InstanceKind,
pub path: PathBuf,
pub launch: LaunchConfiguration,
pub jar_path: Option<PathBuf>,
pub main_class: Option<String>,
pub additional_libs: Vec<PathBuf>,
}
impl InstanceConfiguration {
pub fn new(side: InstanceKind, path: PathBuf) -> Self {
Self {
side,
path,
launch: LaunchConfiguration::new(),
jar_path: None,
main_class: None,
additional_libs: Vec::new(),
}
}
}
pub struct InstanceConfigBuilder {
config: InstanceConfiguration,
}
impl InstanceConfigBuilder {
pub fn new(side: InstanceKind, path: PathBuf) -> Self {
Self {
config: InstanceConfiguration::new(side, path),
}
}
pub fn build(self) -> InstanceConfiguration {
self.config
}
pub fn launch_config(mut self, launch_config: LaunchConfiguration) -> Self {
self.config.launch = launch_config;
self
}
pub fn jar_path(mut self, jar_path: PathBuf) -> Self {
self.config.jar_path = Some(jar_path);
self
}
pub fn main_class(mut self, main_class: String) -> Self {
self.config.main_class = Some(main_class);
self
}
pub fn additional_libs(mut self, additional_libs: Vec<PathBuf>) -> Self {
self.config.additional_libs.extend(additional_libs);
self
}
}
pub enum InstanceKind {
Client {
window: ClientWindowConfig,
},
Server {
create_eula: bool,
show_gui: bool,
},
}
impl InstanceKind {
pub fn get_side(&self) -> Side {
match self {
Self::Client { .. } => Side::Client,
Self::Server { .. } => Side::Server,
}
}
}
#[derive(Default, Clone, Debug)]
pub struct ClientWindowConfig {
pub resolution: Option<WindowResolution>,
}
impl ClientWindowConfig {
pub fn new() -> Self {
Self { resolution: None }
}
}
#[derive(Clone, Debug, Copy)]
pub struct WindowResolution {
pub width: u32,
pub height: u32,
}
impl WindowResolution {
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
}
pub(crate) struct InstanceParameters<'a> {
pub version: &'a VersionName,
pub version_manifest: &'a VersionManifestAndList,
pub paths: &'a Paths,
pub req_client: &'a reqwest::Client,
pub persistent: &'a mut PersistentData,
pub update_manager: &'a mut UpdateManager,
pub client_meta: &'a ClientMeta,
pub users: &'a mut UserManager,
pub client_assets_and_libs: &'a mut ClientAssetsAndLibraries,
pub censor_secrets: bool,
pub disable_hardlinks: bool,
pub branding: &'a BrandingProperties,
}