diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 9c8b3da6..00000000
--- a/.dockerignore
+++ /dev/null
@@ -1,8 +0,0 @@
-.vscode
-target
-.gitignore
-docker-compose.yml
-Dockerfile
-.dockerignore
-node_modules
-.github
diff --git a/.gitignore b/.gitignore
index df65775d..ccadbf92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,8 +15,6 @@ dist
vendor/
vender/
-core/src/game
-
# Created by https://www.toptal.com/developers/gitignore/api/git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all
diff --git a/.rustfmt.toml b/.rustfmt.toml
index b9b2652a..63ac28ed 100644
--- a/.rustfmt.toml
+++ b/.rustfmt.toml
@@ -3,8 +3,7 @@ hard_tabs = true
newline_style = "Unix"
use_small_heuristics = "Default"
reorder_imports = true
-reorder_imported_names = true
-imports_granularity = "Crate"
+imports_granularity = "Module"
reorder_modules = true
remove_nested_parens = true
edition = "2021"
@@ -12,3 +11,4 @@ merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
+unstable_features = true
diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index a8181dec..ba34b132 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -9,15 +9,15 @@ OneLauncher - Open source Rust-based Minecraft Launcher
Copyright (C) 2023~2024 Polyfrost and Contributors
This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, version 3.
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
+GNU Affero General Public License for more details.
-You should have received a copy of the GNU General Public License
+You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
```
diff --git a/Cargo.toml b/Cargo.toml
index ea7e31dc..3bb15d4d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,6 @@ members = [
"debugger",
"desktop/src-tauri",
"testing",
- "backend"
]
[workspace.package]
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0aef2f13..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,30 +0,0 @@
-FROM rustlang/rust:nightly-alpine as builder
-
-WORKDIR /usr/src/polyfrost-api
-
-# Use sparse registry because it is significantly faster and this already requires nightly
-ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true
-
-# Install build tools
-RUN apk add --no-cache g++
-
-# Copy source files
-COPY . .
-
-# Build actual binary
-RUN cargo build --package polyfrost_api --release --locked --all-features
-
-# ---------------------------------------------------------------------------------------------
-
-# :3 :3 :3
-FROM alpine:3
-
-COPY --from=builder /usr/src/polyfrost-api/target/release/polyfrost_api /usr/local/bin/polyfrost_api
-
-# Use an unprivileged user
-RUN adduser --home /nonexistent --no-create-home --disabled-password polyfrost-api
-USER polyfrost-api
-
-HEALTHCHECK --interval=10s --timeout=3s --retries=5 CMD wget --spider --q http://localhost:$PORT/ || exit 1
-
-CMD ["polyfrost_api"]
diff --git a/LICENSE b/LICENSE
index f288702d..5e3e69b5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
+our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
+software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
- "This License" refers to version 3 of the GNU General Public License.
+ "This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
- 13. Use with the GNU Affero General Public License.
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
+under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
+Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
+GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
+versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -635,40 +633,28 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU General Public License
+ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
+For more information on this, and how to apply and follow the GNU AGPL, see
.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
deleted file mode 100644
index 9fc4817a..00000000
--- a/backend/Cargo.toml
+++ /dev/null
@@ -1,34 +0,0 @@
-[package]
-name = "polyfrost_api"
-version = "0.0.0"
-description = "API and Backend for Polyfrost"
-license = { workspace = true }
-edition = { workspace = true }
-repository = { workspace = true }
-documentation = { workspace = true }
-readme = { workspace = true }
-homepage = { workspace = true }
-authors = { workspace = true }
-
-[[bin]]
-name = "polyfrost_api"
-path = "src/main.rs"
-
-[dependencies]
-actix-web = { version = "4" }
-actix-rt = { version = "2.9.0" }
-governor = { version = "0.6" }
-
-tokio = { workspace = true }
-tokio-stream = { workspace = true }
-futures = { workspace = true }
-serde = { workspace = true }
-serde_json = { workspace = true }
-reqwest = { workspace = true }
-thiserror = { workspace = true }
-anyhow = { workspace = true }
-
-log = { version = "0.4" }
-env_logger = { version = "0.11" }
-dotenvy = { version = "0.15" }
-xml = { version = "0.8" }
diff --git a/backend/src/lib.rs b/backend/src/lib.rs
deleted file mode 100644
index e99ffe2a..00000000
--- a/backend/src/lib.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use std::path::Path;
-
-use actix_web::{get, web::Data, HttpResponse, Responder};
-
-#[get("/")]
-pub async fn index() -> impl Responder {
- HttpResponse::Ok().body("Polyfrost API")
-}
-
-#[get("/oneconfig/{version}-{loader}")]
-pub async fn oneconfig(
- data: Data,
- path: Path<(String, String)>,
-) -> impl Responder {
-}
diff --git a/backend/src/main.rs b/backend/src/main.rs
deleted file mode 100644
index a1c25716..00000000
--- a/backend/src/main.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use actix_web::{web::Data, App, HttpServer};
-use log::info;
-
-#[actix_rt::main]
-async fn main() -> std::io::Result<()> {
- dotenvy::dotenv().ok();
- env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
-
- info!(
- "starting polyfrost api on {}",
- dotenvy::var("BIND_ADDR").unwrap()
- );
-
- HttpServer::new(|| {
- let mut headers = reqwest::header::HeaderMap::new();
- let header = reqwest::header::HeaderValue::from_str(&format!(
- "onelauncher/{} (polyfrost.org)",
- env!("CARGO_PKG_VERSION")
- ))
- .unwrap();
- headers.insert(reqwest::header::USER_AGENT, header);
-
- App::new()
- .app_data(Data::new(state::AppState {
- public_maven_url: dotenvy::var("PUBLIC_MAVEN_URL")
- .unwrap_or("https://repo.polyfrost.org".to_string()),
- internal_maven_url: dotenvy::var("INTERNAL_MAVEN_URL")
- .unwrap_or("http://localhost:8000".to_string()),
- http_client: reqwest::Client::builder()
- .tcp_keepalive(Some(std::time::Duration::from_secs(15)))
- .default_headers(headers)
- .build()
- .expect("failed to build reqwest client!"),
- }))
- .service(index)
- .service(oneconfig)
- })
- .bind(dotenvy::var("BIND_ADDR").unwrap())?
- .run()
- .await
-}
diff --git a/backend/src/parsing.rs b/backend/src/parsing.rs
deleted file mode 100644
index e69de29b..00000000
diff --git a/backend/src/state.rs b/backend/src/state.rs
deleted file mode 100644
index e69de29b..00000000
diff --git a/backend/src/utils.rs b/backend/src/utils.rs
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/src/api/cluster/create.rs b/core/src/api/cluster/create.rs
index e69de29b..d9bcfea3 100644
--- a/core/src/api/cluster/create.rs
+++ b/core/src/api/cluster/create.rs
@@ -0,0 +1,266 @@
+//! Utilities for creating Clusters
+
+use crate::data::Loader;
+use crate::package::from::CreatePackCluster;
+use crate::proxy::send::send_cluster;
+use crate::proxy::ClusterPayloadType;
+pub use crate::store::{Cluster, ClusterPath, JavaOptions, PackageData, State};
+use crate::utils::io::{self, canonicalize};
+use crate::{cluster, package};
+use interpulse::api::modded::LoaderVersion;
+use std::path::PathBuf;
+
+/// Creates a [`Cluster`] and adds it to the memory [`State`].
+/// Returns a relative filepath ([`ClusterPath`]) which can be used to access the cluster.
+#[tracing::instrument]
+#[onelauncher_debug::debugger]
+#[allow(clippy::too_many_arguments)]
+pub async fn create_cluster(
+ mut name: String,
+ mc_version: String,
+ mod_loader: Loader,
+ loader_version: Option,
+ icon: Option,
+ icon_url: Option,
+ package_data: Option,
+ skip: Option,
+ skip_watch: Option,
+) -> crate::Result {
+ name = cluster::sanitize_cluster_name(&name);
+ tracing::trace!("creating new cluster {}", name);
+ let state = State::get().await?;
+ let uuid = uuid::Uuid::new_v4();
+ let mut path = state.directories.clusters_dir().await.join(&name);
+
+ // i hate it here
+ if path.exists() {
+ let mut new_name;
+ let mut new_path;
+ let mut which = 1;
+ loop {
+ new_name = format!("{name} ({which})");
+ new_path = state.directories.clusters_dir().await.join(&new_name);
+ if !new_path.exists() {
+ break;
+ }
+ which += 1;
+ }
+
+ tracing::debug!(
+ "collision while creating new cluster: {}, renaming to {}",
+ path.display(),
+ new_path.display()
+ );
+ path = new_path;
+ name = new_name;
+ }
+
+ io::create_dir_all(&path).await?;
+ tracing::info!(
+ "creating cluster at path {}",
+ &canonicalize(&path)?.display()
+ );
+ let loader = if mod_loader != Loader::Vanilla {
+ get_loader_version(mc_version.clone(), mod_loader, loader_version).await?
+ } else {
+ None
+ };
+
+ let mut cluster = Cluster::new(uuid, name, mc_version).await?;
+ let result = async {
+ if let Some(ref icon) = icon {
+ let bytes = io::read(state.directories.caches_dir().await.join(icon)).await?;
+ cluster
+ .set_icon(
+ &state.directories.caches_dir().await,
+ &state.io_semaphore,
+ bytes::Bytes::from(bytes),
+ &icon.to_string_lossy(),
+ )
+ .await?;
+ }
+
+ cluster.meta.icon_url = icon_url;
+ if let Some(loader_version) = loader {
+ cluster.meta.loader = mod_loader;
+ cluster.meta.loader_version = Some(loader_version);
+ }
+
+ cluster.meta.package_data = package_data;
+ if let Some(package_data) = &mut cluster.meta.package_data {
+ package_data.locked =
+ Some(package_data.package_id.is_some() && package_data.version_id.is_some());
+ }
+
+ send_cluster(
+ uuid,
+ &cluster.cluster_path(),
+ &cluster.meta.name,
+ ClusterPayloadType::Created,
+ )
+ .await?;
+
+ {
+ let mut clusters = state.clusters.write().await;
+ clusters
+ .insert(cluster.clone(), skip_watch.unwrap_or_default())
+ .await?;
+ }
+
+ if !skip.unwrap_or(false) {
+ crate::game::install_minecraft(&cluster, None, false).await?;
+ }
+
+ State::sync().await?;
+
+ Ok(cluster.cluster_path())
+ }
+ .await;
+
+ match result {
+ Ok(cluster) => Ok(cluster),
+ Err(err) => {
+ let _ = crate::api::cluster::remove(&cluster.cluster_path()).await;
+
+ Err(err)
+ }
+ }
+}
+
+/// Create a [`Cluster`] from a [`CreatePackCluster`] cluster, returning a [`ClusterPath`].
+pub async fn create_cluster_from_package(cluster: CreatePackCluster) -> crate::Result {
+ create_cluster(
+ cluster.name,
+ cluster.mc_version,
+ cluster.mod_loader,
+ cluster.loader_version,
+ cluster.icon,
+ cluster.icon_url,
+ cluster.package_data,
+ cluster.skip,
+ cluster.skip_watch,
+ )
+ .await
+}
+
+/// Create a duplicate [`Cluster`] from another [`ClusterPath`], returning a new [`ClusterPath`]
+pub async fn create_cluster_from_duplicate(from: ClusterPath) -> crate::Result {
+ let cluster = cluster::get(&from, None).await?.ok_or_else(|| {
+ anyhow::anyhow!("failed to get unmanaged cluster from {}", from.to_string())
+ })?;
+ let cluster_path = create_cluster(
+ cluster.meta.name.clone(),
+ cluster.meta.mc_version.clone(),
+ cluster.meta.loader,
+ cluster.meta.loader_version.clone().map(|it| it.id),
+ cluster.meta.icon.clone(),
+ cluster.meta.icon_url.clone(),
+ cluster.meta.package_data.clone(),
+ Some(true),
+ Some(true),
+ )
+ .await?;
+
+ let state = State::get().await?;
+ let copied = package::import::copy_minecraft(
+ cluster_path.clone(),
+ from.full_path().await?,
+ &state.io_semaphore,
+ None,
+ )
+ .await?;
+
+ let duplicated = cluster::get(&cluster_path, None).await?.ok_or_else(|| {
+ anyhow::anyhow!(
+ "failed to get unmanaged cluster from {}",
+ cluster_path.to_string()
+ )
+ })?;
+
+ crate::game::install_minecraft(&duplicated, Some(copied), false).await?;
+ {
+ let state = State::get().await?;
+ let mut watcher = state.watcher.write().await;
+ Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?;
+ }
+
+ send_cluster(
+ cluster.uuid,
+ &cluster.cluster_path(),
+ &cluster.meta.name,
+ ClusterPayloadType::Edited,
+ )
+ .await?;
+
+ State::sync().await?;
+ Ok(cluster_path)
+}
+
+/// Get the latest [`LoaderVersion`] from a [`Loader`].
+#[tracing::instrument]
+#[onelauncher_debug::debugger]
+pub(crate) async fn get_loader_version(
+ mc_version: String,
+ loader: Loader,
+ loader_version: Option,
+) -> crate::Result> {
+ let state = State::get().await?;
+ let metadata = state.metadata.read().await;
+ let version = loader_version.unwrap_or_else(|| "latest".to_string());
+ let filter = |it: &LoaderVersion| match version.as_str() {
+ "latest" => true,
+ "stable" => it.stable,
+ id => {
+ it.id == *id
+ || format!("{}-{}", mc_version, id) == it.id
+ || format!("{}-{}-{}", mc_version, id, mc_version) == it.id
+ }
+ };
+
+ let loader_meta = match loader {
+ Loader::Forge => &metadata.forge,
+ Loader::Fabric => &metadata.fabric,
+ Loader::Quilt => &metadata.quilt,
+ Loader::NeoForge => &metadata.neoforge,
+ Loader::LegacyFabric => &metadata.legacy_fabric,
+ _ => return Err(CreateClusterError::MissingManifest(loader.to_string()).into()),
+ };
+
+ let loaders = &loader_meta
+ .game_versions
+ .iter()
+ .find(|it| {
+ it.id
+ .replace(interpulse::api::modded::DUMMY_REPLACE_STRING, &mc_version)
+ == mc_version
+ })
+ .ok_or_else(|| {
+ CreateClusterError::UnsupportedLoader(loader.to_string(), mc_version.clone())
+ })?
+ .loaders;
+
+ let loader_version = loaders
+ .iter()
+ .find(|&it| filter(it))
+ .cloned()
+ .or(if version == "stable" {
+ loaders.iter().next().cloned()
+ } else {
+ None
+ })
+ .ok_or_else(|| CreateClusterError::InvalidLoaderVersion(version, loader.to_string()))?;
+
+ Ok(Some(loader_version))
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum CreateClusterError {
+ #[error("Loader {0} is unsupported on Minecraft version {1}")]
+ UnsupportedLoader(String, String),
+ #[error("Invalid Loader version {0} for Loader {1}")]
+ InvalidLoaderVersion(String, String),
+ #[error("Failed to get Loader manifest for {0}.")]
+ MissingManifest(String),
+ #[error("Failed to handle I/O operations: {0}")]
+ IOError(#[from] std::io::Error),
+}
diff --git a/core/src/api/cluster/mod.rs b/core/src/api/cluster/mod.rs
index d3682628..54accfef 100644
--- a/core/src/api/cluster/mod.rs
+++ b/core/src/api/cluster/mod.rs
@@ -1,15 +1,31 @@
-//! OneLauncher cluster management
+//! **OneLauncher Cluster**
+//!
+//! API for our managed Minecraft instances, Clusters.
-use crate::proxy::send::send_cluster;
-use crate::proxy::ClusterPayloadType;
-use crate::prelude::ClusterPath;
-use crate::store::{State, PackagePath};
-use crate::utils::io;
-pub use crate::store::{JavaOptions, Cluster};
+use crate::proxy::send::{init_ingress, send_cluster, send_ingress};
+use crate::proxy::utils::ingress_try_for_each;
+use crate::proxy::{ClusterPayloadType, IngressType};
+use crate::store::ProcessorChild;
+// use crate::package::from::{EnvType, PackDependency, PackFile, PackFileHash, PackFormat};
+use crate::prelude::{ClusterPath, JavaVersion, PackagePath};
+pub use crate::store::{Cluster, JavaOptions, State};
+use crate::store::{InnerPathLinux, MinecraftCredentials, PackageMetadata, PackageSide};
+use crate::utils::http;
+use crate::utils::io::{self, IOError};
-use std::collections::HashMap;
+use async_zip::tokio::write::ZipFileWriter;
+use async_zip::{Compression, ZipEntryBuilder};
+use serde_json::json;
+
+use std::collections::{HashMap, HashSet};
use std::future::Future;
-use std::path::PathBuf;
+use std::iter::FromIterator;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+use tokio::fs::File;
+use tokio::io::AsyncReadExt;
+use tokio::process::Command;
+use tokio::sync::RwLock;
pub mod create;
pub mod update;
@@ -17,117 +33,348 @@ pub mod update;
/// get a cluster by its specified [`ClusterPath`].
#[tracing::instrument]
pub async fn get(path: &ClusterPath, clear: Option) -> crate::Result> {
- let state = State::get().await?;
- let clusters = state.clusters.read().await;
- let mut cluster = clusters.0.get(path).cloned();
+ let state = State::get().await?;
+ let clusters = state.clusters.read().await;
+ let mut cluster = clusters.0.get(path).cloned();
- if clear.unwrap_or(false) {
- if let Some(cluster) = &mut cluster {
- cluster.packages = HashMap::new();
- }
- }
+ if clear.unwrap_or(false) {
+ if let Some(cluster) = &mut cluster {
+ cluster.packages = HashMap::new();
+ }
+ }
- Ok(cluster)
+ Ok(cluster)
+}
+
+/// get a list of all [`Cluster`]s
+#[tracing::instrument]
+pub async fn list(clear: Option) -> crate::Result> {
+ let state = State::get().await?;
+ let clusters = state.clusters.read().await;
+ Ok(clusters
+ .0
+ .clone()
+ .into_iter()
+ .map(|mut it| {
+ if clear.unwrap_or(false) {
+ it.1.packages = HashMap::new();
+ }
+ it
+ })
+ .collect())
+}
+
+/// run a Minecraft [`Cluster`] using the default credentials.
+#[tracing::instrument]
+pub async fn run(path: &ClusterPath) -> crate::Result>> {
+ let state = State::get().await?;
+ let default = {
+ let mut users = state.users.write().await;
+ users
+ .get_default()
+ .await?
+ .ok_or_else(|| anyhow::anyhow!("no default credentials found!"))?
+ };
+
+ run_credentials(path, &default).await
+}
+
+/// run a Minecraft [`Cluster`] using [`MinecraftCredentials`] for authentication.
+/// returns an [`Arc`] pointer to [`RwLock`] to [`ProcessorChild`].
+#[tracing::instrument(skip(creds))]
+#[onelauncher_debug::debugger]
+pub async fn run_credentials(
+ path: &ClusterPath,
+ creds: &MinecraftCredentials,
+) -> crate::Result>> {
+ let state = State::get().await?;
+ let settings = state.settings.read().await;
+ let cluster = get(path, None)
+ .await?
+ .ok_or_else(|| anyhow::anyhow!("failed to run a nonexistent cluster at path {}", path))?;
+
+ let pre = &cluster
+ .init_hooks
+ .as_ref()
+ .unwrap_or(&settings.init_hooks)
+ .pre;
+ if let Some(hook) = pre {
+ // TODO: hook parameters
+ let mut cmd = hook.split(' ');
+ if let Some(command) = cmd.next() {
+ let full_path = path.full_path().await?;
+ let result = Command::new(command)
+ .args(&cmd.collect::>())
+ .current_dir(&full_path)
+ .spawn()
+ .map_err(|e| IOError::with_path(e, &full_path))?
+ .wait()
+ .await
+ .map_err(IOError::from)?;
+
+ if !result.success() {
+ return Err(anyhow::anyhow!(
+ "non-zero exit code for pre-launch hook: {}",
+ result.code().unwrap_or(-1)
+ )
+ .into());
+ }
+ }
+ }
+
+ let java_args = cluster
+ .java
+ .as_ref()
+ .and_then(|it| it.custom_arguments.as_ref())
+ .unwrap_or(&settings.custom_java_args);
+ let wrapper = cluster
+ .init_hooks
+ .as_ref()
+ .map_or(&settings.init_hooks.wrapper, |it| &it.wrapper);
+ let memory = cluster.memory.unwrap_or(settings.memory);
+ let resolution = cluster.resolution.unwrap_or(settings.resolution);
+ let env_args = cluster
+ .java
+ .as_ref()
+ .and_then(|it| it.custom_env_arguments.as_ref())
+ .unwrap_or(&settings.custom_env_args);
+ let post = cluster
+ .init_hooks
+ .as_ref()
+ .unwrap_or(&settings.init_hooks)
+ .post
+ .clone();
+ let mut mc_options: Vec<(String, String)> = vec![];
+ if let Some(fullscreen) = cluster.force_fullscreen {
+ mc_options.push(("fullscreen".to_string(), fullscreen.to_string()));
+ } else if settings.force_fullscreen {
+ mc_options.push(("fullscreen".to_string(), "true".to_string()));
+ }
+
+ let process = crate::game::launch_minecraft(
+ &cluster,
+ java_args,
+ env_args,
+ &mc_options,
+ post,
+ creds,
+ &resolution,
+ &memory,
+ wrapper,
+ )
+ .await?;
+
+ Ok(process)
}
/// remove a specified cluster from it's [`ClusterPath`].
#[tracing::instrument]
pub async fn remove(path: &ClusterPath) -> crate::Result<()> {
- let state = State::get().await?;
- let mut clusters = state.clusters.write().await;
+ let state = State::get().await?;
+ let mut clusters = state.clusters.write().await;
- if let Some(cluster) = clusters.remove(path).await? {
- send_cluster(cluster.uuid, path, &cluster.meta.name, ClusterPayloadType::Deleted).await?;
- }
+ if let Some(cluster) = clusters.remove(path).await? {
+ send_cluster(
+ cluster.uuid,
+ path,
+ &cluster.meta.name,
+ ClusterPayloadType::Deleted,
+ )
+ .await?;
+ }
- Ok(())
+ Ok(())
}
/// get a cluster by it's [`uuid::Uuid`]
#[tracing::instrument]
-pub async fn get_by_uuid(
- uuid: uuid::Uuid,
- clear: Option,
-) -> crate::Result> {
- let state = State::get().await?;
- let clusters = state.clusters.read().await;
- let mut cluster = clusters.0.values().find(|c| c.uuid == uuid).cloned();
+pub async fn get_by_uuid(uuid: uuid::Uuid, clear: Option) -> crate::Result> {
+ let state = State::get().await?;
+ let clusters = state.clusters.read().await;
+ let mut cluster = clusters.0.values().find(|c| c.uuid == uuid).cloned();
- if clear.unwrap_or(false) {
- if let Some(cluster) = &mut cluster {
- cluster.packages = HashMap::new();
- }
- }
+ if clear.unwrap_or(false) {
+ if let Some(cluster) = &mut cluster {
+ cluster.packages = HashMap::new();
+ }
+ }
- Ok(cluster)
+ Ok(cluster)
}
/// get a cluster's full path by it's [`ClusterPath`].
#[tracing::instrument]
pub async fn get_full_path(path: &ClusterPath) -> crate::Result {
- let _ = get(path, Some(true)).await?.ok_or_else(|| anyhow::anyhow!("failed to get the full path of profile at path {}", path))?;
- let full_path = io::canonicalize(path.full_path().await?)?;
+ let _ = get(path, Some(true)).await?.ok_or_else(|| {
+ anyhow::anyhow!("failed to get the full path of profile at path {}", path)
+ })?;
+ let full_path = io::canonicalize(path.full_path().await?)?;
- Ok(full_path)
+ Ok(full_path)
}
/// get a specific mod's full path in the filesystem by it's [`ClusterPath`] and [`PackagePath`].
#[tracing::instrument]
pub async fn get_mod_path(
- cluster_path: &ClusterPath,
- package_path: &PackagePath,
+ cluster_path: &ClusterPath,
+ package_path: &PackagePath,
) -> crate::Result {
- if get(cluster_path, Some(true)).await?.is_some() {
- let full_path = io::canonicalize(package_path.full_path(cluster_path.clone()).await?)?;
+ if get(cluster_path, Some(true)).await?.is_some() {
+ let full_path = io::canonicalize(package_path.full_path(cluster_path.clone()).await?)?;
- return Ok(full_path);
- }
+ return Ok(full_path);
+ }
- Err(anyhow::anyhow!("failed to get the full path of a cluster at path {}", package_path.full_path(cluster_path.clone()).await?.display()).into())
+ Err(anyhow::anyhow!(
+ "failed to get the full path of a cluster at path {}",
+ package_path
+ .full_path(cluster_path.clone())
+ .await?
+ .display()
+ )
+ .into())
}
/// edit a cluster with an async closure and it's [`ClusterPath`]
pub async fn edit(
- path: &ClusterPath,
- action: impl Fn(&mut Cluster) -> FutFn,
+ path: &ClusterPath,
+ action: impl Fn(&mut Cluster) -> FutFn,
) -> crate::Result<()>
where
- FutFn: Future>,
+ FutFn: Future>,
{
- let state = State::get().await?;
- let mut clusters = state.clusters.write().await;
+ let state = State::get().await?;
+ let mut clusters = state.clusters.write().await;
- match clusters.0.get_mut(path) {
- Some(ref mut cluster) => {
- action(cluster).await?;
+ match clusters.0.get_mut(path) {
+ Some(ref mut cluster) => {
+ action(cluster).await?;
- send_cluster(cluster.uuid, path, &cluster.meta.name, ClusterPayloadType::Edited).await?;
+ send_cluster(
+ cluster.uuid,
+ path,
+ &cluster.meta.name,
+ ClusterPayloadType::Edited,
+ )
+ .await?;
- Ok(())
- }
- None => Err(anyhow::anyhow!("unmanaged profile edited at {}", path.to_string()).into())
- }
+ Ok(())
+ }
+ None => Err(anyhow::anyhow!("unmanaged profile edited at {}", path.to_string()).into()),
+ }
}
-/// Try to update a Cluster's playtime.
+/// update a [`Cluster`]'s icon
+pub async fn edit_icon(path: &ClusterPath, icon_path: Option<&Path>) -> crate::Result<()> {
+ let state = State::get().await?;
+ let result = if let Some(icon) = icon_path {
+ let bytes = io::read(icon).await?;
+ let mut clusters = state.clusters.write().await;
+ match clusters.0.get_mut(path) {
+ Some(ref mut cluster) => {
+ cluster
+ .set_icon(
+ &state.directories.caches_dir().await,
+ &state.io_semaphore,
+ bytes::Bytes::from(bytes),
+ &icon.to_string_lossy(),
+ )
+ .await?;
+
+ send_cluster(
+ cluster.uuid,
+ path,
+ &cluster.meta.name,
+ ClusterPayloadType::Edited,
+ )
+ .await?;
+ Ok(())
+ }
+ None => Err(anyhow::anyhow!(
+ "failed to update unmanaged cluster at {}",
+ path.to_string()
+ )
+ .into()),
+ }
+ } else {
+ edit(path, |cluster| {
+ cluster.meta.icon = None;
+ async { Ok(()) }
+ })
+ .await?;
+ State::sync().await?;
+
+ Ok(())
+ };
+
+ State::sync().await?;
+ result
+}
+
+/// gets the optimal java version for a given [`Cluster`].
+pub async fn get_optimal_java_version(path: &ClusterPath) -> crate::Result> {
+ let state = State::get().await?;
+ if let Some(cluster) = get(path, None).await? {
+ let metadata = state.metadata.read().await;
+ let version = metadata
+ .minecraft
+ .versions
+ .iter()
+ .find(|it| it.id == cluster.meta.mc_version)
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "invalid or unknown Minecraft version {}",
+ cluster.meta.mc_version
+ )
+ })?;
+
+ let version_info = crate::game::metadata::download_version_info(
+ &state,
+ version,
+ cluster.meta.loader_version.as_ref(),
+ None,
+ None,
+ )
+ .await?;
+
+ let version = crate::game::java_version_from_cluster(&cluster, &version_info).await?;
+
+ Ok(version)
+ } else {
+ Err(anyhow::anyhow!(
+ "failed to get the java version of unmanaged cluster at {}",
+ path.to_string()
+ )
+ .into())
+ }
+}
+
+/// Try to update a [`Cluster`]'s playtime.
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn update_playtime(path: &ClusterPath) -> crate::Result<()> {
- let state = State::get().await?;
- let cluster = get(path, None).await?.ok_or_else(|| anyhow::anyhow!("failed to update playtime at path {}", path))?;
- let recent_playtime = cluster.meta.recently_played;
-
- /*
- * todo
- */
+ let state = State::get().await?;
+ let cluster = get(path, None)
+ .await?
+ .ok_or_else(|| anyhow::anyhow!("failed to update playtime at path {}", path))?;
+ let recent_playtime = cluster.meta.recently_played;
- let mut clusters = state.clusters.write().await;
- if let Some(cluster) = clusters.0.get_mut(path) {
- cluster.meta.overall_played += recent_playtime;
- cluster.meta.recently_played = 0;
- }
+ /*
+ * todo
+ */
- State::sync().await?;
+ let mut clusters = state.clusters.write().await;
+ if let Some(cluster) = clusters.0.get_mut(path) {
+ cluster.meta.overall_played += recent_playtime;
+ cluster.meta.recently_played = 0;
+ }
- Ok(())
+ State::sync().await?;
+
+ Ok(())
+}
+
+/// Sanitize a user-inputted [`Cluster`] name.
+pub fn sanitize_cluster_name(input: &str) -> String {
+ input.replace(['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>'], "_")
}
diff --git a/core/src/api/cluster/update.rs b/core/src/api/cluster/update.rs
index e69de29b..8e60c619 100644
--- a/core/src/api/cluster/update.rs
+++ b/core/src/api/cluster/update.rs
@@ -0,0 +1 @@
+//! Utilities for updating Clusters
diff --git a/core/src/api/handler.rs b/core/src/api/handler.rs
index 7a104af6..378b93fb 100644
--- a/core/src/api/handler.rs
+++ b/core/src/api/handler.rs
@@ -1,42 +1,42 @@
use std::path::PathBuf;
+use crate::api::prelude::InternetPayload;
use crate::proxy::send::{send_internet, send_message};
use crate::utils::io;
-use crate::api::prelude::InternetPayload;
pub async fn parse_uri(cmd: &str) -> crate::Result<()> {
- let cmd = handle_cmd(cmd).await?;
- send_internet(cmd).await?;
- Ok(())
+ let cmd = handle_cmd(cmd).await?;
+ send_internet(cmd).await?;
+ Ok(())
}
pub async fn handle_cmd(cmd: &str) -> crate::Result {
- tracing::debug!("parsing command {}", &cmd);
+ tracing::debug!("parsing command {}", &cmd);
- if let Some(sub) = cmd.strip_prefix("onelauncher://") {
- Ok(handle_uri(sub).await?)
- } else {
- let path = io::canonicalize(PathBuf::from(cmd))?;
- if let Some(extension) = path.extension() {
- if extension == "zip" {
- return Ok(InternetPayload::InstallPath { path })
- }
- }
- send_message(&format!("invalid command filetype {}", path.display())).await?;
- Err(anyhow::anyhow!("invalid command filename {}", path.display()).into())
- }
+ if let Some(sub) = cmd.strip_prefix("onelauncher://") {
+ Ok(handle_uri(sub).await?)
+ } else {
+ let path = io::canonicalize(PathBuf::from(cmd))?;
+ if let Some(extension) = path.extension() {
+ if extension == "zip" {
+ return Ok(InternetPayload::InstallPath { path });
+ }
+ }
+ send_message(&format!("invalid command filetype {}", path.display())).await?;
+ Err(anyhow::anyhow!("invalid command filename {}", path.display()).into())
+ }
}
pub async fn handle_uri(sub: &str) -> crate::Result {
- Ok(match sub.split_once('/') {
- // ://pkg/{id} - installs package id
- Some(("pkg", id)) => InternetPayload::InstallPackage { id: id.to_string() },
- // ://pack/{id} - installs pack id
- Some(("pack", id)) => InternetPayload::InstallPack { id: id.to_string() },
- // unknown uri command: error
- _ => {
- send_message(&format!("invalid command {sub}")).await?;
- return Err(anyhow::anyhow!("invalid command {sub}").into());
- }
- })
+ Ok(match sub.split_once('/') {
+ // ://pkg/{id} - installs package id
+ Some(("pkg", id)) => InternetPayload::InstallPackage { id: id.to_string() },
+ // ://pack/{id} - installs pack id
+ Some(("pack", id)) => InternetPayload::InstallPack { id: id.to_string() },
+ // unknown uri command: error
+ _ => {
+ send_message(&format!("invalid command {sub}")).await?;
+ return Err(anyhow::anyhow!("invalid command {sub}").into());
+ }
+ })
}
diff --git a/core/src/api/ingress.rs b/core/src/api/ingress.rs
index 78736214..8ba5fd71 100644
--- a/core/src/api/ingress.rs
+++ b/core/src/api/ingress.rs
@@ -1,5 +1,5 @@
use crate::store::{IngressProcessType, IngressProcessor};
pub async fn check_ingress_feeds() -> crate::Result {
- IngressProcessor::finished(IngressProcessType::IngressFeed).await
+ IngressProcessor::finished(IngressProcessType::IngressFeed).await
}
diff --git a/core/src/api/java.rs b/core/src/api/java.rs
index b530ebce..0ea9c1ec 100644
--- a/core/src/api/java.rs
+++ b/core/src/api/java.rs
@@ -4,117 +4,137 @@ use reqwest::Method;
use serde::Deserialize;
use crate::proxy::send::{init_ingress, send_ingress};
-use crate::utils::{http::{fetch_advanced, fetch_json}, io::{self, IOError}, java::{self, get_java_version, JavaVersion}};
+use crate::utils::http::{fetch_advanced, fetch_json};
+use crate::utils::io::{self, IOError};
+use crate::utils::java::{self, get_java_version, JavaVersion};
use crate::State;
-pub async fn filter_java_version(
- java_version: Option,
-) -> crate::Result> {
- let java = java::locate_java().await?;
- Ok(if let Some(java_version) = java_version {
- java.into_iter().filter(|j| {
- let jre_version = get_java_version(&j.version);
- if let Ok(jre_version) = jre_version {
- jre_version.1 == java_version
- } else {
- false
- }
- }).collect()
- } else {
- java
- })
+pub async fn filter_java_version(java_version: Option) -> crate::Result> {
+ let java = java::locate_java().await?;
+ Ok(if let Some(java_version) = java_version {
+ java.into_iter()
+ .filter(|j| {
+ let jre_version = get_java_version(&j.version);
+ if let Ok(jre_version) = jre_version {
+ jre_version.1 == java_version
+ } else {
+ false
+ }
+ })
+ .collect()
+ } else {
+ java
+ })
}
#[onelauncher_debug::debugger]
pub async fn install_java(java_version: u32) -> crate::Result {
- let state = State::get().await?;
+ let state = State::get().await?;
- let ingress = init_ingress(
- crate::IngressType::DownloadJava { version: java_version },
- 100.0, "downloading java version").await?;
+ let ingress = init_ingress(
+ crate::IngressType::DownloadJava {
+ version: java_version,
+ },
+ 100.0,
+ "downloading java version",
+ )
+ .await?;
- #[derive(Deserialize)]
- struct Package {
- pub download_url: String,
- pub name: PathBuf,
- }
+ #[derive(Deserialize)]
+ struct Package {
+ pub download_url: String,
+ pub name: PathBuf,
+ }
- send_ingress(&ingress, 0.0, Some("fetching java api")).await?;
- let packages = fetch_json::>(Method::GET, &format!(
+ send_ingress(&ingress, 0.0, Some("fetching java api")).await?;
+ let packages = fetch_json::>(Method::GET, &format!(
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
std::env::consts::ARCH, java_version, std::env::consts::OS,
), None, None, &state.fetch_semaphore).await?;
- send_ingress(&ingress, 10.0, Some("downloading java version")).await?;
+ send_ingress(&ingress, 10.0, Some("downloading java version")).await?;
- if let Some(download) = packages.first() {
- let file = fetch_advanced(
- Method::GET,
- &download.download_url,
- None,
- None,
- None,
- Some((&ingress, 80.0)),
- &state.fetch_semaphore,
- ).await?;
+ if let Some(download) = packages.first() {
+ let file = fetch_advanced(
+ Method::GET,
+ &download.download_url,
+ None,
+ None,
+ None,
+ Some((&ingress, 80.0)),
+ &state.fetch_semaphore,
+ )
+ .await?;
- let path = state.directories.java_dir().await;
- let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
- .map_err(|err| IOError::from_zip(err))?;
+ let path = state.directories.java_dir().await;
+ let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
+ .map_err(|err| IOError::from_zip(err))?;
- if let Some(file) = archive.file_names().next() {
- if let Some(dir) = file.split('/').next() {
- let path = path.join(dir);
- if path.exists() { io::remove_dir_all(path).await?; }
- }
- }
+ if let Some(file) = archive.file_names().next() {
+ if let Some(dir) = file.split('/').next() {
+ let path = path.join(dir);
+ if path.exists() {
+ io::remove_dir_all(path).await?;
+ }
+ }
+ }
- send_ingress(&ingress, 0.0, Some("extracing java binary")).await?;
- archive.extract(&path).map_err(|err| IOError::from_zip(err))?;
- send_ingress(&ingress, 10.0, Some("extracted java binary")).await?;
- let mut base_path = path.join(download.name.file_stem().unwrap_or_default().to_string_lossy().to_string());
+ send_ingress(&ingress, 0.0, Some("extracing java binary")).await?;
+ archive
+ .extract(&path)
+ .map_err(|err| IOError::from_zip(err))?;
+ send_ingress(&ingress, 10.0, Some("extracted java binary")).await?;
+ let mut base_path = path.join(
+ download
+ .name
+ .file_stem()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .to_string(),
+ );
- #[cfg(target_os = "macos")]
- {
- base_path = base_path
- .join(format!("zulu-{}.jre", java_version))
- .join("Contents")
- .join("Home")
- .join("bin")
- .join("java")
- }
+ #[cfg(target_os = "macos")]
+ {
+ base_path = base_path
+ .join(format!("zulu-{}.jre", java_version))
+ .join("Contents")
+ .join("Home")
+ .join("bin")
+ .join("java")
+ }
- #[cfg(not(target_os = "macos"))]
- {
- base_path = base_path.join("bin").join(java::JAVA_BIN)
- }
+ #[cfg(not(target_os = "macos"))]
+ {
+ base_path = base_path.join("bin").join(java::JAVA_BIN)
+ }
- Ok(base_path)
- } else {
- Err(anyhow::anyhow!(
- "no java version found for version {}, os {}, and arch {}",
- java_version, std::env::consts::OS, std::env::consts::ARCH,
- ).into())
- }
+ Ok(base_path)
+ } else {
+ Err(anyhow::anyhow!(
+ "no java version found for version {}, os {}, and arch {}",
+ java_version,
+ std::env::consts::OS,
+ std::env::consts::ARCH,
+ )
+ .into())
+ }
}
pub async fn check_java(path: PathBuf) -> crate::Result> {
- Ok(java::check_java_instance(&path).await)
+ Ok(java::check_java_instance(&path).await)
}
-pub async fn test_java(
- path: PathBuf,
- major: u32,
-) -> crate::Result {
- let jvm = match java::check_java_instance(&path).await {
- Some(jvm) => jvm,
- None => return Ok(false),
- };
+pub async fn test_java(path: PathBuf, major: u32) -> crate::Result {
+ let jvm = match java::check_java_instance(&path).await {
+ Some(jvm) => jvm,
+ None => return Ok(false),
+ };
- let (maj, _) = get_java_version(&jvm.version)?;
- Ok(maj == major)
+ let (maj, _) = get_java_version(&jvm.version)?;
+ Ok(maj == major)
}
pub async fn get_max_memory() -> crate::Result {
- Ok(sys_info::mem_info()
- .map_err(|_| anyhow::anyhow!("failed to get computer memory"))?.total)
+ Ok(sys_info::mem_info()
+ .map_err(|_| anyhow::anyhow!("failed to get computer memory"))?
+ .total)
}
diff --git a/core/src/api/logger.rs b/core/src/api/logger.rs
index 90032c0c..26d9a441 100644
--- a/core/src/api/logger.rs
+++ b/core/src/api/logger.rs
@@ -1,198 +1,286 @@
//! OneLauncher log management
-use std::io::Read;
-use tokio::io::{AsyncReadExt, AsyncSeekExt};
-use serde::Serialize;
-use futures::TryFutureExt;
use crate::data::{Credentials, Directories, MinecraftCredentials};
use crate::prelude::ClusterPath;
use crate::utils::io::{self, IOError};
+use futures::TryFutureExt;
+use serde::Serialize;
+use std::io::Read;
+use tokio::io::{AsyncReadExt, AsyncSeekExt};
// TODO: put this in the global store
/// Core logging state and reader for OneLauncher.
#[derive(Serialize, Debug)]
pub struct LogManager {
- /// The log file to read as a [`String`]
- pub log_file: String,
- /// The parsed and censored output of the logfile.
- pub output: Option
+ /// The log file to read as a [`String`]
+ pub log_file: String,
+ /// The parsed and censored output of the logfile.
+ pub output: Option,
}
impl LogManager {
- /// Initialize a new [`LogManager`].
- async fn initialize(cluster: &ClusterPath, log_file: String, clear: Option) -> crate::Result {
- Ok(Self {
- output: if clear.unwrap_or(false) {
- None
- } else {
- Some(get_output_by_file(cluster, &log_file).await?)
- },
- log_file,
- })
- }
+ /// Initialize a new [`LogManager`].
+ async fn initialize(
+ cluster: &ClusterPath,
+ log_file: String,
+ clear: Option,
+ ) -> crate::Result {
+ Ok(Self {
+ output: if clear.unwrap_or(false) {
+ None
+ } else {
+ Some(get_output_by_file(cluster, &log_file).await?)
+ },
+ log_file,
+ })
+ }
}
/// Get a [`Vec`] for a specific [`ClusterPath`].
#[tracing::instrument]
-pub async fn get_logs(cluster_path: ClusterPath, clear: Option) -> crate::Result> {
- let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
- c.cluster_path()
- } else {
- return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into());
- };
+pub async fn get_logs(
+ cluster_path: ClusterPath,
+ clear: Option,
+) -> crate::Result> {
+ let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
+ c.cluster_path()
+ } else {
+ return Err(anyhow::anyhow!(
+ "cluster {} isn't managed by onelauncher!",
+ cluster_path.to_string()
+ )
+ .into());
+ };
- let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
- let mut logs = Vec::new();
- if logs_folder.exists() {
- for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? {
- let entry: std::fs::DirEntry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
- let path = entry.path();
- if !path.is_file() { continue; }
- if let Some(name) = path.file_name() {
- let name = name.to_string_lossy().to_string();
- logs.push(LogManager::initialize(&cluster_path, name, clear).await);
- }
- }
- }
+ let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
+ let mut logs = Vec::new();
+ if logs_folder.exists() {
+ for entry in
+ std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))?
+ {
+ let entry: std::fs::DirEntry =
+ entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
+ let path = entry.path();
+ if !path.is_file() {
+ continue;
+ }
+ if let Some(name) = path.file_name() {
+ let name = name.to_string_lossy().to_string();
+ logs.push(LogManager::initialize(&cluster_path, name, clear).await);
+ }
+ }
+ }
- let mut logs = logs.into_iter().collect::>>()?;
- logs.sort_by_key(|x| x.log_file.clone());
- Ok(logs)
+ let mut logs = logs
+ .into_iter()
+ .collect::>>()?;
+ logs.sort_by_key(|x| x.log_file.clone());
+ Ok(logs)
}
/// Delete all stored logs from a specific [`ClusterPath`].
#[tracing::instrument]
pub async fn delete_logs(cluster_path: ClusterPath) -> crate::Result<()> {
- let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
- c.cluster_path()
- } else {
- return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into());
- };
+ let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
+ c.cluster_path()
+ } else {
+ return Err(anyhow::anyhow!(
+ "cluster {} isn't managed by onelauncher!",
+ cluster_path.to_string()
+ )
+ .into());
+ };
- let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
- for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? {
- let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
- let path = entry.path();
- if path.is_dir() { io::remove_dir_all(&path).await? }
- }
+ let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
+ for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? {
+ let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
+ let path = entry.path();
+ if path.is_dir() {
+ io::remove_dir_all(&path).await?
+ }
+ }
- Ok(())
+ Ok(())
}
/// Get the [`LogManager`] for a specific [`ClusterPath`] log file.
#[tracing::instrument]
-pub async fn get_logs_by_file(cluster_path: ClusterPath, log_file: String) -> crate::Result {
- let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
- c.cluster_path()
- } else {
- return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into());
- };
+pub async fn get_logs_by_file(
+ cluster_path: ClusterPath,
+ log_file: String,
+) -> crate::Result {
+ let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
+ c.cluster_path()
+ } else {
+ return Err(anyhow::anyhow!(
+ "cluster {} isn't managed by onelauncher!",
+ cluster_path.to_string()
+ )
+ .into());
+ };
- Ok(LogManager {
- output: Some(get_output_by_file(&cluster_path, &log_file).await?),
- log_file,
- })
+ Ok(LogManager {
+ output: Some(get_output_by_file(&cluster_path, &log_file).await?),
+ log_file,
+ })
}
/// Get the default [`LogCursor`] for a [`ClusterPath`].
#[tracing::instrument]
pub async fn get_log_cursor(cluster_path: ClusterPath, cursor: u64) -> crate::Result {
- get_live_log_cursor(cluster_path, "latest.log", cursor).await
+ get_live_log_cursor(cluster_path, "latest.log", cursor).await
}
/// Get a live [`LogCursor`] for a [`ClusterPath`]'s log file.
#[tracing::instrument]
-pub async fn get_live_log_cursor(cluster_path: ClusterPath, log_file: &str, mut cursor: u64) -> crate::Result {
- let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
- c.cluster_path()
- } else {
- return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into());
- };
+pub async fn get_live_log_cursor(
+ cluster_path: ClusterPath,
+ log_file: &str,
+ mut cursor: u64,
+) -> crate::Result {
+ let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
+ c.cluster_path()
+ } else {
+ return Err(anyhow::anyhow!(
+ "cluster {} isn't managed by onelauncher!",
+ cluster_path.to_string()
+ )
+ .into());
+ };
- let state = crate::State::get().await?;
- let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
- let path = logs_folder.join(log_file);
- if !path.exists() { return Ok(LogCursor { cursor: 0, new: false, output: LogOutput("".to_string()) }); }
+ let state = crate::State::get().await?;
+ let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
+ let path = logs_folder.join(log_file);
+ if !path.exists() {
+ return Ok(LogCursor {
+ cursor: 0,
+ new: false,
+ output: LogOutput("".to_string()),
+ });
+ }
- let mut file = tokio::fs::File::open(&path).await.map_err(|e| IOError::with_path(e, &path))?;
- let metadata = file.metadata().await.map_err(|e| IOError::with_path(e, &path))?;
- let mut new = false;
+ let mut file = tokio::fs::File::open(&path)
+ .await
+ .map_err(|e| IOError::with_path(e, &path))?;
+ let metadata = file
+ .metadata()
+ .await
+ .map_err(|e| IOError::with_path(e, &path))?;
+ let mut new = false;
- if cursor > metadata.len() {
- cursor = 0;
- new = true;
- }
+ if cursor > metadata.len() {
+ cursor = 0;
+ new = true;
+ }
- let mut buf = Vec::new();
- file.seek(std::io::SeekFrom::Start(cursor)).map_err(|e| IOError::with_path(e, &path)).await?;
- let bytes_read = file.read_to_end(&mut buf).map_err(|e| IOError::with_path(e, &path)).await?;
- let output = String::from_utf8_lossy(&buf).to_string();
- let cursor = cursor + bytes_read as u64;
- let creds: Vec = state.users.read().await.users.clone().into_values().collect();
- let output = LogOutput::censor_secrets(output, &creds, None);
- Ok(LogCursor { cursor, new, output })
+ let mut buf = Vec::new();
+ file.seek(std::io::SeekFrom::Start(cursor))
+ .map_err(|e| IOError::with_path(e, &path))
+ .await?;
+ let bytes_read = file
+ .read_to_end(&mut buf)
+ .map_err(|e| IOError::with_path(e, &path))
+ .await?;
+ let output = String::from_utf8_lossy(&buf).to_string();
+ let cursor = cursor + bytes_read as u64;
+ let creds: Vec = state
+ .users
+ .read()
+ .await
+ .users
+ .clone()
+ .into_values()
+ .collect();
+ let output = LogOutput::censor_secrets(output, &creds, None);
+ Ok(LogCursor {
+ cursor,
+ new,
+ output,
+ })
}
/// Delete a specific minecraft log file.
#[tracing::instrument]
pub async fn delete_logs_by_file(cluster_path: ClusterPath, log_file: &str) -> crate::Result<()> {
- let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
- c.cluster_path()
- } else {
- return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into());
- };
+ let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
+ c.cluster_path()
+ } else {
+ return Err(anyhow::anyhow!(
+ "cluster {} isn't managed by onelauncher!",
+ cluster_path.to_string()
+ )
+ .into());
+ };
- let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
- let path = logs_folder.join(log_file);
- io::remove_dir_all(&path).await?;
- Ok(())
+ let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
+ let path = logs_folder.join(log_file);
+ io::remove_dir_all(&path).await?;
+ Ok(())
}
/// Get a specific [`ClusterPath`] log file's [`LogOutput`].
#[tracing::instrument]
-pub async fn get_output_by_file(cluster_path: &ClusterPath, log_file: &str) -> crate::Result {
- let state = crate::State::get().await?;
- let logs_folder = Directories::cluster_logs_dir(cluster_path).await?;
- let path = logs_folder.join(log_file);
- let credentials: Vec = state.users.read().await.users.clone().into_values().collect();
+pub async fn get_output_by_file(
+ cluster_path: &ClusterPath,
+ log_file: &str,
+) -> crate::Result {
+ let state = crate::State::get().await?;
+ let logs_folder = Directories::cluster_logs_dir(cluster_path).await?;
+ let path = logs_folder.join(log_file);
+ let credentials: Vec = state
+ .users
+ .read()
+ .await
+ .users
+ .clone()
+ .into_values()
+ .collect();
- // todo: make this a utility function
- if let Some(ext) = path.extension() {
- let mut file = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?;
- let mut contents = [0; 1024];
- let mut result = String::new();
+ // todo: make this a utility function
+ if let Some(ext) = path.extension() {
+ let mut file = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?;
+ let mut contents = [0; 1024];
+ let mut result = String::new();
- if ext == "gz" {
- let mut gz = flate2::read::GzDecoder::new(std::io::BufReader::new(file));
+ if ext == "gz" {
+ let mut gz = flate2::read::GzDecoder::new(std::io::BufReader::new(file));
- while gz.read(&mut contents).map_err(|e| IOError::with_path(e, &path))? > 0 {
- result.push_str(&String::from_utf8_lossy(&contents));
- contents = [0; 1024];
- }
+ while gz
+ .read(&mut contents)
+ .map_err(|e| IOError::with_path(e, &path))?
+ > 0
+ {
+ result.push_str(&String::from_utf8_lossy(&contents));
+ contents = [0; 1024];
+ }
- return Ok(LogOutput::censor_secrets(result, &credentials, None));
- } else if ext == "log" {
- while file.read(&mut contents).map_err(|e| IOError::with_path(e, &path))? > 0 {
- result.push_str(&String::from_utf8_lossy(&contents));
- contents = [0; 1024];
- }
+ return Ok(LogOutput::censor_secrets(result, &credentials, None));
+ } else if ext == "log" {
+ while file
+ .read(&mut contents)
+ .map_err(|e| IOError::with_path(e, &path))?
+ > 0
+ {
+ result.push_str(&String::from_utf8_lossy(&contents));
+ contents = [0; 1024];
+ }
- return Ok(LogOutput::censor_secrets(result, &credentials, None));
- }
- }
+ return Ok(LogOutput::censor_secrets(result, &credentials, None));
+ }
+ }
- Err(anyhow::anyhow!("log file extension {} not supported", path.display()).into())
+ Err(anyhow::anyhow!("log file extension {} not supported", path.display()).into())
}
/// The log cursor used to parse logs passed into [`LogManager`].
#[derive(Serialize, Debug)]
pub struct LogCursor {
- /// The cursor ID.
- pub cursor: u64,
- /// The [`LogOutput`] associated with this cursor.
- pub output: LogOutput,
- /// Whether or not this corresponds is a new log file.
- pub new: bool,
+ /// The cursor ID.
+ pub cursor: u64,
+ /// The [`LogOutput`] associated with this cursor.
+ pub output: LogOutput,
+ /// Whether or not this corresponds is a new log file.
+ pub new: bool,
}
/// The log output, a wrapper around [`String`], with utilities for parsing and censoring log contents.
@@ -201,22 +289,26 @@ pub struct LogCursor {
pub struct LogOutput(String);
impl LogOutput {
- /// Censor user secrets because sometimes mclogs misses them, including username, realname and minecraft credentials.
- pub fn censor_secrets(mut output: String, credentials: &Vec, credentials_store: Option) -> Self {
- let username = whoami::username();
- let realname = whoami::realname();
- output = output.replace(&format!("/{}/", username), "/{ENV_USERNAME}/");
- output = output.replace(&format!("\\{}\\", username), "\\{ENV_USERNAME}\\");
- output = output.replace(&format!("/{}/", realname), "/{ENV_REALNAME}/");
- output = output.replace(&format!("\\{}\\", realname), "\\{ENV_REALNAME}\\");
+ /// Censor user secrets because sometimes mclogs misses them, including username, realname and minecraft credentials.
+ pub fn censor_secrets(
+ mut output: String,
+ credentials: &Vec,
+ credentials_store: Option,
+ ) -> Self {
+ let username = whoami::username();
+ let realname = whoami::realname();
+ output = output.replace(&format!("/{}/", username), "/{ENV_USERNAME}/");
+ output = output.replace(&format!("\\{}\\", username), "\\{ENV_USERNAME}\\");
+ output = output.replace(&format!("/{}/", realname), "/{ENV_REALNAME}/");
+ output = output.replace(&format!("\\{}\\", realname), "\\{ENV_REALNAME}\\");
- for cred in credentials {
- output = output.replace(&cred.access_token, "{MC_ACCESS_TOKEN}");
- output = output.replace(&cred.username, "{MC_USERNAME}");
- output = output.replace(&cred.id.as_simple().to_string(), "{MC_UUID}");
- output = output.replace(&cred.id.as_hyphenated().to_string(), "{MC_UUID}");
- }
+ for cred in credentials {
+ output = output.replace(&cred.access_token, "{MC_ACCESS_TOKEN}");
+ output = output.replace(&cred.username, "{MC_USERNAME}");
+ output = output.replace(&cred.id.as_simple().to_string(), "{MC_UUID}");
+ output = output.replace(&cred.id.as_hyphenated().to_string(), "{MC_UUID}");
+ }
- Self(output)
- }
+ Self(output)
+ }
}
diff --git a/core/src/api/metadata.rs b/core/src/api/metadata.rs
index e39d5726..267c2d4e 100644
--- a/core/src/api/metadata.rs
+++ b/core/src/api/metadata.rs
@@ -7,55 +7,53 @@ pub use interpulse::api::modded::Manifest;
/// Get a [`VersionManifest`] for all available Minecraft versions.
#[tracing::instrument]
pub async fn get_minecraft_versions() -> crate::Result {
- let state = State::get().await?;
- let meta = state.metadata.read().await.minecraft.clone();
+ let state = State::get().await?;
+ let meta = state.metadata.read().await.minecraft.clone();
- Ok(meta)
+ Ok(meta)
}
/// Get a [`Manifest`] for all available Fabric versions.
#[tracing::instrument]
pub async fn get_fabric_versions() -> crate::Result {
- let state = State::get().await?;
- let meta = state.metadata.read().await.fabric.clone();
+ let state = State::get().await?;
+ let meta = state.metadata.read().await.fabric.clone();
- Ok(meta)
+ Ok(meta)
}
/// Get a [`Manifest`] for all available Quilt versions.
#[tracing::instrument]
pub async fn get_quilt_versions() -> crate::Result {
- let state = State::get().await?;
- let meta = state.metadata.read().await.quilt.clone();
+ let state = State::get().await?;
+ let meta = state.metadata.read().await.quilt.clone();
- Ok(meta)
+ Ok(meta)
}
/// Get a [`Manifest`] for all available Forge versions.
#[tracing::instrument]
pub async fn get_forge_versions() -> crate::Result {
- let state = State::get().await?;
- let meta = state.metadata.read().await.forge.clone();
+ let state = State::get().await?;
+ let meta = state.metadata.read().await.forge.clone();
- Ok(meta)
+ Ok(meta)
}
/// Get a [`Manifest`] for all available NeoForge versions.
#[tracing::instrument]
pub async fn get_neoforge_versions() -> crate::Result {
- let state = State::get().await?;
- let meta = state.metadata.read().await.neoforge.clone();
+ let state = State::get().await?;
+ let meta = state.metadata.read().await.neoforge.clone();
- Ok(meta)
+ Ok(meta)
}
/// Get a [`Manifest`] for all available Legacy Fabric versions.
#[tracing::instrument]
pub async fn get_legacy_fabric_versions() -> crate::Result {
- let state = State::get().await?;
- let meta = state.metadata.read().await.legacy_fabric.clone();
+ let state = State::get().await?;
+ let meta = state.metadata.read().await.legacy_fabric.clone();
- Ok(meta)
+ Ok(meta)
}
-
-
diff --git a/core/src/api/minecraft.rs b/core/src/api/minecraft.rs
index 14a951d9..1e68542d 100644
--- a/core/src/api/minecraft.rs
+++ b/core/src/api/minecraft.rs
@@ -5,73 +5,76 @@ use crate::store::{MinecraftCredentials, MinecraftLogin, State};
/// Begin a Microsoft authentication flow.
#[tracing::instrument]
pub async fn begin() -> crate::Result {
- let state = State::get().await?;
- let mut users = state.users.write().await;
+ let state = State::get().await?;
+ let mut users = state.users.write().await;
- users.begin().await
+ users.begin().await
}
/// Complete a Microsoft authentication flow to recieve [`MinecraftCredentials`].
#[tracing::instrument]
pub async fn finish(code: &str, flow: MinecraftLogin) -> crate::Result {
- let state = State::get().await?;
- let mut users = state.users.write().await;
+ let state = State::get().await?;
+ let mut users = state.users.write().await;
- users.finish(code, flow).await
+ users.finish(code, flow).await
}
/// Get the current default user if it exists by [`uuid::Uuid`].
#[tracing::instrument]
pub async fn get_default_user() -> crate::Result> {
- let state = State::get().await?;
- let users = state.users.read().await;
+ let state = State::get().await?;
+ let users = state.users.read().await;
- Ok(users.default_user)
+ Ok(users.default_user)
}
/// Set the current default user by [`uuid::Uuid`].
#[tracing::instrument]
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
- let user = get_user(user).await?;
- let state = State::get().await?;
- let mut users = state.users.write().await;
+ let user = get_user(user).await?;
+ let state = State::get().await?;
+ let mut users = state.users.write().await;
- users.default_user = Some(user.id);
- users.save().await?;
+ users.default_user = Some(user.id);
+ users.save().await?;
- Ok(())
+ Ok(())
}
/// Remove a user account by its [`uuid::Uuid`] from the global store.
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
- let state = State::get().await?;
- let mut users = state.users.write().await;
-
- users.remove(user).await?;
+ let state = State::get().await?;
+ let mut users = state.users.write().await;
- Ok(())
+ users.remove(user).await?;
+
+ Ok(())
}
// TODO: Store this securely in stronghold
/// Get a list of user [`MinecraftCredentials`].
#[tracing::instrument]
pub async fn users() -> crate::Result> {
- let state = State::get().await?;
- let users = state.users.read().await;
+ let state = State::get().await?;
+ let users = state.users.read().await;
- Ok(users.users.values().cloned().collect())
+ Ok(users.users.values().cloned().collect())
}
/// Get a specifc user's [`MinecraftCredentials`] by their [`uuid::Uuid`].
/// Use [`crate::store::MinecraftState#refresh`] instead.
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result {
- let state = State::get().await?;
- let users = state.users.read().await;
+ let state = State::get().await?;
+ let users = state.users.read().await;
- let user = users.users.get(&user)
- .ok_or_else(|| anyhow::anyhow!("failed to get nonexistent user with uuid {user}"))?.clone();
+ let user = users
+ .users
+ .get(&user)
+ .ok_or_else(|| anyhow::anyhow!("failed to get nonexistent user with uuid {user}"))?
+ .clone();
- Ok(user)
+ Ok(user)
}
diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs
index 2d8480d4..b21c9937 100644
--- a/core/src/api/mod.rs
+++ b/core/src/api/mod.rs
@@ -1,34 +1,33 @@
//! **OneLauncher API**
-//!
+//!
//! API for interacting with our Rust core.
-pub mod java;
-pub mod proxy;
-pub mod handler;
-pub mod settings;
-pub mod ingress;
-pub mod processor;
-pub mod minecraft;
-pub mod metadata;
-pub mod logger;
-pub mod package;
pub mod cluster;
+pub mod handler;
+pub mod ingress;
+pub mod java;
+pub mod logger;
+pub mod metadata;
+pub mod minecraft;
+pub mod package;
+pub mod processor;
+pub mod proxy;
+pub mod settings;
pub mod data {
- pub use crate::store::{
- Credentials, MinecraftCredentials, Directories, InitHooks, JavaOptions,
- PackageData, Memory, Loader, ClusterMeta, Settings, Theme, Resolution,
- PackageType, ManagedDependency, ManagedPackage, ManagedUser, ManagedVersion,
- };
+ pub use crate::store::{
+ ClusterMeta, Credentials, Directories, InitHooks, JavaOptions, Loader, ManagedDependency,
+ ManagedPackage, ManagedUser, ManagedVersion, Memory, MinecraftCredentials, PackageData,
+ PackageType, Resolution, Settings, Theme,
+ };
}
pub mod prelude {
- pub use crate::{
- data::*,
- proxy::InternetPayload,
- java, metadata, minecraft, package, processor,
- settings, store::JavaVersions, store::ClusterPath,
- utils::io::{canonicalize, IOError}, utils::java::JavaVersion,
- State,
- };
+ pub use crate::cluster::{self, create, Cluster};
+ pub use crate::data::*;
+ pub use crate::proxy::InternetPayload;
+ pub use crate::store::{ClusterPath, JavaVersions, PackageDependency, PackagePath};
+ pub use crate::utils::io::{canonicalize, IOError};
+ pub use crate::utils::java::JavaVersion;
+ pub use crate::{java, metadata, minecraft, package, processor, settings, State};
}
diff --git a/core/src/api/package/from.rs b/core/src/api/package/from.rs
index e69de29b..4e51c6d3 100644
--- a/core/src/api/package/from.rs
+++ b/core/src/api/package/from.rs
@@ -0,0 +1,20 @@
+//! Install modpacks from different sources.
+
+use crate::data::{Loader, PackageData};
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+/// Modpack associated [`Cluster`] wrapper.
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreatePackCluster {
+ pub name: String,
+ pub mc_version: String,
+ pub mod_loader: Loader,
+ pub loader_version: Option,
+ pub icon: Option,
+ pub icon_url: Option,
+ pub package_data: Option,
+ pub skip: Option,
+ pub skip_watch: Option,
+}
diff --git a/core/src/api/package/import/atlauncher.rs b/core/src/api/package/import/atlauncher.rs
index e69de29b..8b137891 100644
--- a/core/src/api/package/import/atlauncher.rs
+++ b/core/src/api/package/import/atlauncher.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/curseforge.rs b/core/src/api/package/import/curseforge.rs
index e69de29b..8b137891 100644
--- a/core/src/api/package/import/curseforge.rs
+++ b/core/src/api/package/import/curseforge.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/ftb.rs b/core/src/api/package/import/ftb.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/core/src/api/package/import/ftb.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/gdlauncher.rs b/core/src/api/package/import/gdlauncher.rs
index e69de29b..8b137891 100644
--- a/core/src/api/package/import/gdlauncher.rs
+++ b/core/src/api/package/import/gdlauncher.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/mod.rs b/core/src/api/package/import/mod.rs
index 776d3866..1704cd5d 100644
--- a/core/src/api/package/import/mod.rs
+++ b/core/src/api/package/import/mod.rs
@@ -1,91 +1,281 @@
//! Manages importing data from other launchers.
-use std::{fmt, path::{Path, PathBuf}};
-use serde::{Serialize, Deserialize};
-use crate::{prelude::ClusterPath, utils::io::{self, IOError}};
+use crate::prelude::ClusterPath;
+use crate::proxy::send::{init_or_edit_ingress, send_ingress};
+use crate::proxy::IngressId;
+use crate::store::Clusters;
+use crate::utils::http::{self, IoSemaphore};
+use crate::utils::io::{self, IOError};
+use serde::{Deserialize, Serialize};
+use std::fmt;
+use std::path::{Path, PathBuf};
pub mod atlauncher;
pub mod curseforge;
+pub mod ftb;
pub mod gdlauncher;
+pub mod modrinth;
pub mod multibased;
+pub mod technic;
+pub mod tlauncher;
/// List of launcher types we support importing from.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ImportType {
- /// MultiMC based launchers
- MultiMC,
- /// Prism Launcher has its own category because it has different logic (also is objectively better than mmc)
- PrismLauncher,
- /// GDLauncher
- GDLauncher,
- /// Curseforge's launcher
- Curseforge,
- /// ATLauncher
- ATLauncher,
- /// Modrinth app.
- Modrinth,
- /// Unknown import option (not widely adopted -> probably a custom launcher with a similar file structure to the above)
- #[serde(other)]
- Unknown,
+ /// MultiMC based launchers
+ MultiMC,
+ /// Prism Launcher has its own category because it has different logic (also is objectively better than mmc)
+ PrismLauncher,
+ /// GDLauncher
+ GDLauncher,
+ /// Curseforge's launcher
+ Curseforge,
+ /// ATLauncher
+ ATLauncher,
+ /// Modrinth app.
+ Modrinth,
+ /// TLauncher
+ TLauncher,
+ /// FTB Launcher
+ FTBLauncher,
+ /// Technic
+ Technic,
+ /// Unknown import option (not widely adopted -> probably a custom launcher with a similar file structure to the above)
+ #[serde(other)]
+ Unknown,
}
impl fmt::Display for ImportType {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ImportType::MultiMC => write!(f, "MultiMC"),
- ImportType::ATLauncher => write!(f, "ATLauncher"),
- ImportType::Curseforge => write!(f, "Curseforge"),
- ImportType::GDLauncher => write!(f, "GDLauncher"),
- ImportType::Modrinth => write!(f, "Modrinth"),
- ImportType::PrismLauncher => write!(f, "PrismLauncher"),
- ImportType::Unknown => write!(f, "Unknown"),
- }
- }
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ImportType::MultiMC => write!(f, "MultiMC"),
+ ImportType::ATLauncher => write!(f, "ATLauncher"),
+ ImportType::Curseforge => write!(f, "Curseforge"),
+ ImportType::GDLauncher => write!(f, "GDLauncher"),
+ ImportType::Modrinth => write!(f, "Modrinth"),
+ ImportType::PrismLauncher => write!(f, "PrismLauncher"),
+ ImportType::TLauncher => write!(f, "TLauncher"),
+ ImportType::FTBLauncher => write!(f, "FTB"),
+ ImportType::Technic => write!(f, "Technic"),
+ ImportType::Unknown => write!(f, "Unknown"),
+ }
+ }
}
pub async fn import_instances(import: ImportType, path: PathBuf) -> crate::Result> {
- let instances_path = match import {
- ImportType::GDLauncher | ImportType::ATLauncher => "instances".to_string(),
- ImportType::Curseforge => "Instances".to_string(),
- ImportType::MultiMC => multibased::get_instances_path(path.clone().join("multimc.cfg")).await.unwrap_or_else(|| "instances".to_string()),
- ImportType::PrismLauncher => multibased::get_instances_path(path.clone().join("prismlauncher.cfg")).await.unwrap_or_else(|| "instances".to_string()),
- ImportType::Modrinth => "Profiles".to_string(),
- ImportType::Unknown | ImportType::Unknown => return Err(anyhow::anyhow!("launcher type unknown, cant import").into()),
- };
+ let instances_path = match import {
+ ImportType::GDLauncher | ImportType::ATLauncher => "instances".to_string(),
+ ImportType::Curseforge => "Instances".to_string(),
+ ImportType::MultiMC => multibased::get_instances_path(path.clone().join("multimc.cfg"))
+ .await
+ .unwrap_or_else(|| "instances".to_string()),
+ ImportType::PrismLauncher => {
+ multibased::get_instances_path(path.clone().join("prismlauncher.cfg"))
+ .await
+ .unwrap_or_else(|| "instances".to_string())
+ }
+ ImportType::Modrinth => "profiles".to_string(),
+ ImportType::TLauncher => "instances".to_string(),
+ ImportType::Technic => "instances".to_string(),
+ ImportType::FTBLauncher => "instances".to_string(),
+ ImportType::Unknown | ImportType::Unknown => {
+ return Err(anyhow::anyhow!("launcher type unknown, cant import").into())
+ }
+ };
- let instances_dir = path.join(&instances_path);
- let mut instances = Vec::new();
- let mut dir = io::read_dir(&instances_dir).await.map_err(|_| {
- anyhow::anyhow!("invalid {import} launcher path, failed to import.")
- })?;
+ let instances_dir = path.join(&instances_path);
+ let mut instances = Vec::new();
+ let mut dir = io::read_dir(&instances_dir)
+ .await
+ .map_err(|_| anyhow::anyhow!("invalid {import} launcher path, failed to import."))?;
- while let Some(e) = dir.next_entry().await.map_err(|e| IOError::with_path(e, &instances_dir))? {
- let path = e.path();
- if path.is_dir() {
- if is_valid_instance(path.clone(), import).await {
- let name = path.file_name();
- if let Some(name) = name {
- instances.push(name.to_string_lossy().to_string());
- }
- }
- }
- }
+ while let Some(e) = dir
+ .next_entry()
+ .await
+ .map_err(|e| IOError::with_path(e, &instances_dir))?
+ {
+ let path = e.path();
+ if path.is_dir() {
+ if is_valid_instance(path.clone(), import).await {
+ let name = path.file_name();
+ if let Some(name) = name {
+ instances.push(name.to_string_lossy().to_string());
+ }
+ }
+ }
+ }
- Ok(instances)
+ Ok(instances)
}
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn import_instance(
- cluster_path: ClusterPath,
- import: ImportType,
- path: PathBuf,
- instance_path: String,
+ cluster_path: ClusterPath,
+ import: ImportType,
+ path: PathBuf,
+ instance_path: String,
) -> crate::Result<()> {
- tracing::debug!("importing instance from {instance_path}");
-
- tracing::debug!("completed import of instance.");
- Ok(())
+ tracing::debug!("importing instance from {instance_path}");
+
+ let result = match import {
+ ImportType::MultiMC | ImportType::PrismLauncher => {
+ multibased::import(path, instance_path, cluster_path.clone()).await
+ }
+ ImportType::ATLauncher => {
+ atlauncher::import(path, instance_path, cluster_path.clone()).await
+ }
+ ImportType::GDLauncher => {
+ gdlauncher::import(
+ path.join("instances").join(instance_path),
+ cluster_path.clone(),
+ )
+ .await
+ }
+ ImportType::Curseforge => {
+ curseforge::import(
+ path.join("Instances").join(instance_path),
+ cluster_path.clone(),
+ )
+ .await
+ }
+ ImportType::Modrinth => {
+ modrinth::import(
+ path.join("profiles").join(instance_path),
+ cluster_path.clone(),
+ )
+ .await
+ }
+ ImportType::TLauncher => {
+ tlauncher::import(
+ path.join("instances").join(instance_path),
+ cluster_path.clone(),
+ )
+ .await
+ }
+ ImportType::Technic => {
+ technic::import(
+ path.join("instances").join(instance_path),
+ cluster_path.clone(),
+ )
+ .await
+ }
+ ImportType::FTBLauncher => {
+ ftb::import(
+ path.join("instances").join(instance_path),
+ cluster_path.clone(),
+ )
+ .await
+ }
+ ImportType::Unknown => {
+ return Err(anyhow::anyhow!("unknown launcher type").into());
+ }
+ };
+
+ match result {
+ Ok(_) => {}
+ Err(e) => {
+ tracing::warn!("failed to import modpack: {:?}", e);
+ let _ = crate::api::cluster::remove(&cluster_path).await;
+ return Err(e);
+ }
+ }
+
+ tokio::task::spawn(Clusters::update_versions());
+
+ tracing::debug!("completed import of instance.");
+ Ok(())
+}
+
+/// returns the default path for a given [`ImportType`].
+pub fn default_launcher_path(r#type: ImportType) -> Option {
+ let path = match r#type {
+ ImportType::MultiMC => None,
+ ImportType::PrismLauncher => Some(dirs::data_dir()?.join("PrismLauncher")),
+ ImportType::ATLauncher => Some(dirs::data_dir()?.join("ATLauncher")),
+ ImportType::GDLauncher => Some(dirs::data_dir()?.join("gdlauncher_next")),
+ ImportType::Curseforge => Some(dirs::home_dir()?.join("curseforge").join("minecraft")),
+ ImportType::Modrinth => Some(dirs::data_dir()?.join("theseus")),
+ ImportType::FTBLauncher => Some(dirs::data_dir()?.join("FTB")),
+ ImportType::Technic => Some(dirs::data_dir()?.join("Technic")),
+ ImportType::TLauncher => Some(dirs::data_dir()?.join("TLauncher")),
+ ImportType::Unknown => None,
+ };
+
+ let path = path?;
+ if path.exists() {
+ Some(path)
+ } else {
+ None
+ }
+}
+
+/// checks if a [`PathBuf`] is a valid instance for a given [`ImportType`]
+#[tracing::instrument]
+#[onelauncher_debug::debugger]
+pub async fn is_valid_instance(instance_path: PathBuf, r#type: ImportType) -> bool {
+ match r#type {
+ ImportType::MultiMC | ImportType::PrismLauncher => multibased::is_valid(insance_path).await,
+ ImportType::ATLauncher => atlauncher::is_valid(instance_path).await,
+ ImportType::GDLauncher => gdlauncher::is_valid(instance_path).await,
+ ImportType::Curseforge => curseforge::is_valid(instance_path).await,
+ ImportType::Modrinth => modrinth::is_valid(instance_path).await,
+ ImportType::TLauncher => tlauncher::is_valid(instance_path).await,
+ ImportType::FTBLauncher => ftb::is_valid(instance_path).await,
+ ImportType::Technic => technic::is_valid(instance_path).await,
+ ImportType::Unknown => false,
+ }
+}
+
+/// caches an image file
+#[tracing::instrument]
+#[onelauncher_debug::debugger]
+pub async fn cache_icon(icon_path: PathBuf) -> crate::Result> {
+ let state = crate::State::get().await?;
+ let bytes = tokio::fs::read(&icon_path).await;
+ if let Ok(bytes) = bytes {
+ let bytes = bytes::Bytes::from(bytes);
+ let cache_dir = &state.directories.caches_dir().await;
+ let semaphore = &state.io_semaphore;
+ Ok(Some(
+ http::write_icon(&icon_path.to_string_lossy(), cache_dir, bytes, semaphore).await?,
+ ))
+ } else {
+ Ok(None)
+ }
+}
+
+pub async fn copy_minecraft(
+ cluster_path: ClusterPath,
+ minecraft_path: PathBuf,
+ io_semaphore: &IoSemaphore,
+ old_ingress: Option,
+) -> crate::Result {
+ let cluster_full = cluster_path.full_path().await?;
+ let subfiles = sub(&minecraft_path).await?;
+ let total_subfiles = subfiles.len() as u64;
+ let ingress = init_or_edit_ingress(
+ old_ingress,
+ crate::IngressType::CopyCluster {
+ import: minecraft_path.clone(),
+ cluster_name: cluster_path.to_string(),
+ },
+ total_subfiles as f64,
+ "copying files in preexisting cluster",
+ )
+ .await?;
+
+ for sub in subfiles {
+ let child = sub
+ .strip_prefix(&minecraft_path)
+ .map_err(|_| anyhow::anyhow!("invalid .minecraft file {}", &sub.display()))?;
+ let child = cluster_path.0.join(child);
+ tokio::time::sleep(std::time::Duration::from_millis(1)).await;
+ http::copy(&sub, &child, io_semaphore).await?;
+ send_ingress(&ingress, 1.0, None).await?;
+ }
+
+ Ok(ingress)
}
/// recursively get a [`Vec`] of all subfiles.
@@ -93,13 +283,19 @@ pub async fn import_instance(
#[async_recursion::async_recursion]
#[tracing::instrument]
pub async fn sub(path: &Path) -> crate::Result> {
- if !path.is_dir() { return Ok(vec![path.to_path_buf()]); }
- let mut files = Vec::new();
- let mut dir = io::read_dir(&path).await?;
- while let Some(child) = dir.next_entry().await.map_err(|e| IOError::with_path(e, path))? {
- let path_child = child.path();
- files.append(&mut sub(&path_child).await?);
- }
-
- Ok(files)
+ if !path.is_dir() {
+ return Ok(vec![path.to_path_buf()]);
+ }
+ let mut files = Vec::new();
+ let mut dir = io::read_dir(&path).await?;
+ while let Some(child) = dir
+ .next_entry()
+ .await
+ .map_err(|e| IOError::with_path(e, path))?
+ {
+ let path_child = child.path();
+ files.append(&mut sub(&path_child).await?);
+ }
+
+ Ok(files)
}
diff --git a/core/src/api/package/import/modrinth.rs b/core/src/api/package/import/modrinth.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/core/src/api/package/import/modrinth.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/multibased.rs b/core/src/api/package/import/multibased.rs
index e69de29b..8b137891 100644
--- a/core/src/api/package/import/multibased.rs
+++ b/core/src/api/package/import/multibased.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/technic.rs b/core/src/api/package/import/technic.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/core/src/api/package/import/technic.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/import/tlauncher.rs b/core/src/api/package/import/tlauncher.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/core/src/api/package/import/tlauncher.rs
@@ -0,0 +1 @@
+
diff --git a/core/src/api/package/mod.rs b/core/src/api/package/mod.rs
index 2438e36b..79f529a7 100644
--- a/core/src/api/package/mod.rs
+++ b/core/src/api/package/mod.rs
@@ -1,5 +1,8 @@
-//! OneLauncher modpack management
+//! **OneLauncher Package**
+//!
+//! Utilities for importing packages from and to OneLauncher.
pub mod from;
-pub mod mrpack;
pub mod import;
+pub mod mrpack;
+pub mod zip;
diff --git a/core/src/api/package/mrpack.rs b/core/src/api/package/mrpack.rs
index e69de29b..4ad96dfe 100644
--- a/core/src/api/package/mrpack.rs
+++ b/core/src/api/package/mrpack.rs
@@ -0,0 +1 @@
+//! Import and Export `.mrpack` mod packs.
diff --git a/core/src/api/package/zip.rs b/core/src/api/package/zip.rs
index e69de29b..4f8a87a9 100644
--- a/core/src/api/package/zip.rs
+++ b/core/src/api/package/zip.rs
@@ -0,0 +1 @@
+//! Import and Export `.zip` mod packs
diff --git a/core/src/api/processor.rs b/core/src/api/processor.rs
index 32fe5da3..a2f86116 100644
--- a/core/src/api/processor.rs
+++ b/core/src/api/processor.rs
@@ -1,110 +1,107 @@
//! Process management interface
+pub use crate::store::{Cluster, InitHooks, JavaOptions, Memory, Resolution, Settings, State};
+use crate::store::{ClusterPath, ProcessorChild};
use uuid::Uuid;
-use crate::store::{ProcessorChild, ClusterPath};
-pub use crate::store::{InitHooks, JavaOptions, Memory, Cluster, Settings, Resolution, State};
/// check whether or not a process has completed by its [`Uuid`].
#[tracing::instrument]
pub async fn uuid_is_finished(uuid: &Uuid) -> crate::Result {
- Ok(uuid_exit_status(uuid).await?.is_some())
+ Ok(uuid_exit_status(uuid).await?.is_some())
}
/// check the exit status of a process by its [`Uuid`].
#[tracing::instrument]
pub async fn uuid_exit_status(uuid: &Uuid) -> crate::Result> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
- processor.exit_status(uuid).await
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
+ processor.exit_status(uuid).await
}
/// get all process [`Uuid`]s.
#[tracing::instrument]
pub async fn get_uuids() -> crate::Result> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
- Ok(processor.keys())
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
+ Ok(processor.keys())
}
/// get all running process [`Uuid`]s.
#[tracing::instrument]
pub async fn get_running() -> crate::Result> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
- processor.running().await
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
+ processor.running().await
}
/// get all running process' [`ClusterPath`]s.
#[tracing::instrument]
-pub async fn get_running_cluster_paths() -> crate::Result>
-{
- let state = State::get().await?;
- let processor = state.processor.read().await;
- processor.running_cluster_paths().await
+pub async fn get_running_cluster_paths() -> crate::Result> {
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
+ processor.running_cluster_paths().await
}
/// get all running process' [`Cluster`]s.
#[tracing::instrument]
pub async fn get_running_clusters() -> crate::Result> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
- processor.running_clusters().await
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
+ processor.running_clusters().await
}
/// get all processes by a [`ClusterPath`].
#[tracing::instrument]
-pub async fn get_uuids_by_cluster_path(
- cluster_path: ClusterPath,
-) -> crate::Result> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
- processor.running_cluster(cluster_path).await
+pub async fn get_uuids_by_cluster_path(cluster_path: ClusterPath) -> crate::Result> {
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
+ processor.running_cluster(cluster_path).await
}
/// kill an existing and running process by its [`Uuid`]
#[tracing::instrument]
pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
- if let Some(process) = processor.get(uuid) {
- let mut process = process.write().await;
- kill(&mut process).await
- } else {
- Ok(())
- }
+ if let Some(process) = processor.get(uuid) {
+ let mut process = process.write().await;
+ kill(&mut process).await
+ } else {
+ Ok(())
+ }
}
/// wait for a child process to finish running its manager by its [`Uuid`].
#[tracing::instrument]
pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
- let state = State::get().await?;
- let processor = state.processor.read().await;
+ let state = State::get().await?;
+ let processor = state.processor.read().await;
- if let Some(process) = processor.get(uuid) {
- let mut process = process.write().await;
- wait_for(&mut process).await
- } else {
- Ok(())
- }
+ if let Some(process) = processor.get(uuid) {
+ let mut process = process.write().await;
+ wait_for(&mut process).await
+ } else {
+ Ok(())
+ }
}
/// kill an existing and running process
#[tracing::instrument(skip(process))]
pub async fn kill(process: &mut ProcessorChild) -> crate::Result<()> {
- process.current_child.write().await.kill().await?;
- Ok(())
+ process.current_child.write().await.kill().await?;
+ Ok(())
}
/// wait for a child process to finish running its manager
#[tracing::instrument(skip(process))]
pub async fn wait_for(process: &mut ProcessorChild) -> crate::Result<()> {
- process
- .manager
- .take()
- .ok_or_else(||anyhow::anyhow!("manager already completed for process {}", process.uuid))?
- .await?
- .map_err(|err| anyhow::anyhow!("failed to run minecraft: {err}"))?;
+ process
+ .manager
+ .take()
+ .ok_or_else(|| anyhow::anyhow!("manager already completed for process {}", process.uuid))?
+ .await?
+ .map_err(|err| anyhow::anyhow!("failed to run minecraft: {err}"))?;
- Ok(())
+ Ok(())
}
diff --git a/core/src/api/proxy/mod.rs b/core/src/api/proxy/mod.rs
index b41ecdfc..15791554 100644
--- a/core/src/api/proxy/mod.rs
+++ b/core/src/api/proxy/mod.rs
@@ -1,8 +1,10 @@
//! Proxy for Tauri/CLI specific features and communication
use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock};
use uuid::Uuid;
-use std::{collections::HashMap, path::PathBuf, sync::Arc};
use crate::store::{ClusterPath, IngressProcessor};
@@ -12,243 +14,240 @@ pub use utils::*;
static PROXY_STATE: OnceCell> = OnceCell::const_new();
pub struct ProxyState {
- #[cfg(feature = "tauri")]
- pub app: tauri::AppHandle,
- pub ingress_feeds: RwLock>,
+ #[cfg(feature = "tauri")]
+ pub app: tauri::AppHandle,
+ pub ingress_feeds: RwLock>,
}
#[derive(Serialize, Debug, Clone)]
pub struct Ingress {
- pub ingress_uuid: Uuid,
- pub message: String,
- pub ingress_type: IngressType,
- pub total: f64,
- pub current: f64,
- #[serde(skip)]
- pub last_sent: f64,
- #[cfg(feature = "cli")]
- #[serde(skip)]
- pub cli: indicatif::ProgressBar,
+ pub ingress_uuid: Uuid,
+ pub message: String,
+ pub ingress_type: IngressType,
+ pub total: f64,
+ pub current: f64,
+ #[serde(skip)]
+ pub last_sent: f64,
+ #[cfg(feature = "cli")]
+ #[serde(skip)]
+ pub cli: indicatif::ProgressBar,
}
#[derive(Serialize, Debug, Clone)]
pub struct IngressId(Uuid);
impl Drop for IngressId {
- fn drop(&mut self) {
- let ingress_uuid = self.0;
- tokio::spawn(async move {
- if let Ok(proxy_state) = ProxyState::get().await {
- let mut ingress_feeds = proxy_state.ingress_feeds.write().await;
- #[cfg(any(feature = "tauri", feature = "cli"))]
- if let Some(ingress) = ingress_feeds.remove(&ingress_uuid) {
- #[cfg(feature = "cli")]
- {
- let cli = ingress.cli;
- cli.finish();
- }
+ fn drop(&mut self) {
+ let ingress_uuid = self.0;
+ tokio::spawn(async move {
+ if let Ok(proxy_state) = ProxyState::get().await {
+ let mut ingress_feeds = proxy_state.ingress_feeds.write().await;
+ #[cfg(any(feature = "tauri", feature = "cli"))]
+ if let Some(ingress) = ingress_feeds.remove(&ingress_uuid) {
+ #[cfg(feature = "cli")]
+ {
+ let cli = ingress.cli;
+ cli.finish();
+ }
- #[cfg(feature = "tauri")]
- {
- let ingress_feed_uuid = ingress.ingress_uuid;
- let event = ingress.ingress_type.clone();
- let fraction = ingress.current / ingress.total;
+ #[cfg(feature = "tauri")]
+ {
+ let ingress_feed_uuid = ingress.ingress_uuid;
+ let event = ingress.ingress_type.clone();
+ let fraction = ingress.current / ingress.total;
- use tauri::Manager;
- let _ = proxy_state.app.emit("ingress", IngressPayload {
- fraction: None,
- message: "Complete".to_string(),
- event,
- ingress_uuid,
- });
+ use tauri::Manager;
+ let _ = proxy_state.app.emit(
+ "ingress",
+ IngressPayload {
+ fraction: None,
+ message: "Complete".to_string(),
+ event,
+ ingress_uuid,
+ },
+ );
- tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid);
- }
- }
+ tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid);
+ }
+ }
- #[cfg(not(any(feature = "tauri", feature = "cli")))]
- ingress_feeds.remove(&ingress_uuid);
- }
+ #[cfg(not(any(feature = "tauri", feature = "cli")))]
+ ingress_feeds.remove(&ingress_uuid);
+ }
- if crate::State::initalized() {
- let _ = IngressProcessor::finish(
- crate::store::IngressProcessType::IngressFeed,
- ingress_uuid,
- )
- .await;
- }
- });
- }
+ if crate::State::initalized() {
+ let _ = IngressProcessor::finish(
+ crate::store::IngressProcessType::IngressFeed,
+ ingress_uuid,
+ )
+ .await;
+ }
+ });
+ }
}
impl ProxyState {
- #[cfg(feature = "tauri")]
- pub async fn initialize(app: tauri::AppHandle) -> crate::Result> {
- PROXY_STATE
- .get_or_try_init(|| async {
- Ok(Arc::new(Self {
- app,
- ingress_feeds: RwLock::new(HashMap::new()),
- }))
- })
- .await
- .cloned()
- }
+ #[cfg(feature = "tauri")]
+ pub async fn initialize(app: tauri::AppHandle) -> crate::Result> {
+ PROXY_STATE
+ .get_or_try_init(|| async {
+ Ok(Arc::new(Self {
+ app,
+ ingress_feeds: RwLock::new(HashMap::new()),
+ }))
+ })
+ .await
+ .cloned()
+ }
- #[cfg(not(feature = "tauri"))]
- pub async fn initialize() -> crate::Result> {
- PROXY_STATE
- .get_or_try_init(|| async {
- Ok(Arc::new(Self {
- ingress_feeds: RwLock::new(HashMap::new()),
- }))
- })
- .await
- .cloned()
- }
+ #[cfg(not(feature = "tauri"))]
+ pub async fn initialize() -> crate::Result> {
+ PROXY_STATE
+ .get_or_try_init(|| async {
+ Ok(Arc::new(Self {
+ ingress_feeds: RwLock::new(HashMap::new()),
+ }))
+ })
+ .await
+ .cloned()
+ }
- #[cfg(feature = "tauri")]
- pub async fn get_main_window() -> crate::Result {
- use tauri::Manager;
- let value = Self::get().await?;
- Ok(value.app.get_webview_window("main").unwrap())
- }
+ #[cfg(feature = "tauri")]
+ pub async fn get_main_window() -> crate::Result {
+ use tauri::Manager;
+ let value = Self::get().await?;
+ Ok(value.app.get_webview_window("main").unwrap())
+ }
- #[cfg(feature = "tauri")]
- pub async fn get() -> crate::Result> {
- Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
- }
+ #[cfg(feature = "tauri")]
+ pub async fn get() -> crate::Result> {
+ Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
+ }
- #[cfg(not(feature = "tauri"))]
- pub async fn get() -> crate::Result> {
- Self::initialize().await
- }
+ #[cfg(not(feature = "tauri"))]
+ pub async fn get() -> crate::Result> {
+ Self::initialize().await
+ }
- pub async fn list_ingress_feeds() -> crate::Result> {
- let value = Self::get().await?;
- let read = value.ingress_feeds.read().await;
+ pub async fn list_ingress_feeds() -> crate::Result> {
+ let value = Self::get().await?;
+ let read = value.ingress_feeds.read().await;
- let mut display_list: HashMap = HashMap::new();
- for (uuid, ingress) in read.iter() {
- display_list.insert(*uuid, ingress.clone());
- }
+ let mut display_list: HashMap = HashMap::new();
+ for (uuid, ingress) in read.iter() {
+ display_list.insert(*uuid, ingress.clone());
+ }
- Ok(display_list)
- }
+ Ok(display_list)
+ }
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum IngressType {
- Initialize,
- DownloadJava {
- version: u32,
- },
- DownloadLoader {
- cluster_path: PathBuf,
- cluster_name: String,
- },
- SyncCluster {
- cluster_path: PathBuf,
- cluster_name: String,
- },
- CopyCluster {
- import: PathBuf,
- cluster_name: String,
- },
- SyncConfig {
- new_path: PathBuf,
- },
- Archival {
- cluster_path: PathBuf,
- cluster_name: String,
- },
- DownloadPackage {
- cluster_path: PathBuf,
- package_name: String,
- icon: Option,
- package_id: Option,
- package_version: Option
- }
+ Initialize,
+ DownloadJava {
+ version: u32,
+ },
+ DownloadLoader {
+ cluster_path: PathBuf,
+ cluster_name: String,
+ },
+ SyncCluster {
+ cluster_path: PathBuf,
+ cluster_name: String,
+ },
+ CopyCluster {
+ import: PathBuf,
+ cluster_name: String,
+ },
+ SyncConfig {
+ new_path: PathBuf,
+ },
+ Archival {
+ cluster_path: PathBuf,
+ cluster_name: String,
+ },
+ DownloadPackage {
+ cluster_path: PathBuf,
+ package_name: String,
+ icon: Option,
+ package_id: Option,
+ package_version: Option,
+ },
}
#[derive(Serialize, Clone)]
pub struct IngressPayload {
- pub event: IngressType,
- pub ingress_uuid: Uuid,
- pub fraction: Option,
- pub message: String,
+ pub event: IngressType,
+ pub ingress_uuid: Uuid,
+ pub fraction: Option,
+ pub message: String,
}
#[derive(Serialize, Clone)]
pub struct OfflinePayload {
- pub offline: bool,
+ pub offline: bool,
}
#[derive(Serialize, Clone)]
pub struct MessagePayload {
- pub message: String,
+ pub message: String,
}
#[derive(Serialize, Clone)]
#[serde(tag = "event")]
pub enum InternetPayload {
- InstallPackage {
- id: String,
- },
- InstallPack {
- id: String,
- },
- InstallPath {
- path: PathBuf,
- },
+ InstallPackage { id: String },
+ InstallPack { id: String },
+ InstallPath { path: PathBuf },
}
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
- pub uuid: Uuid,
- pub pid: u32,
- pub event: ProcessPayloadType,
- pub message: String,
+ pub uuid: Uuid,
+ pub pid: u32,
+ pub event: ProcessPayloadType,
+ pub message: String,
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPayloadType {
- Started,
- Modified,
- Finished,
+ Started,
+ Modified,
+ Finished,
}
#[derive(Serialize, Clone)]
pub struct ClusterPayload {
- pub uuid: Uuid,
- pub cluster_path: ClusterPath,
- pub path: PathBuf,
- pub name: String,
- pub event: ClusterPayloadType,
+ pub uuid: Uuid,
+ pub cluster_path: ClusterPath,
+ pub path: PathBuf,
+ pub name: String,
+ pub event: ClusterPayloadType,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ClusterPayloadType {
- Created,
- Inserted,
- Synced,
- Edited,
- Deleted,
+ Created,
+ Inserted,
+ Synced,
+ Edited,
+ Deleted,
}
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
- #[error("event state was not initialized!")]
- NotInitialized,
+ #[error("event state was not initialized!")]
+ NotInitialized,
- #[error("non-existant ingress of key: {0}")]
- NoIngressFound(Uuid),
+ #[error("non-existant ingress of key: {0}")]
+ NoIngressFound(Uuid),
- #[cfg(feature = "tauri")]
- #[error("Tauri error: {0}")]
- TauriError(#[from] tauri::Error),
+ #[cfg(feature = "tauri")]
+ #[error("Tauri error: {0}")]
+ TauriError(#[from] tauri::Error),
}
diff --git a/core/src/api/proxy/send.rs b/core/src/api/proxy/send.rs
index a800336b..4449fcd0 100644
--- a/core/src/api/proxy/send.rs
+++ b/core/src/api/proxy/send.rs
@@ -1,23 +1,25 @@
use super::IngressId;
-use crate::api::proxy::{ProxyError, Ingress, IngressType, ProcessPayloadType, ClusterPayloadType, InternetPayload};
+use crate::api::proxy::{
+ ClusterPayloadType, Ingress, IngressType, InternetPayload, ProcessPayloadType, ProxyError,
+};
use crate::store::{ClusterPath, IngressProcessType, IngressProcessor};
use uuid::Uuid;
#[cfg(feature = "tauri")]
-use crate::api::proxy::{IngressPayload, ProcessPayload, ClusterPayload, MessagePayload};
+use crate::api::proxy::{ClusterPayload, IngressPayload, MessagePayload, ProcessPayload};
#[cfg(feature = "tauri")]
use tauri::Manager;
#[onelauncher_debug::debugger]
pub async fn init_ingress(
- ingress_type: IngressType,
- total: f64,
- title: &str,
+ ingress_type: IngressType,
+ total: f64,
+ title: &str,
) -> crate::Result {
- let key = init_ingress_internal(ingress_type, total, title).await?;
- IngressProcessor::add_ingress(IngressProcessType::IngressFeed, key.0).await?;
- Ok(key)
+ let key = init_ingress_internal(ingress_type, total, title).await?;
+ IngressProcessor::add_ingress(IngressProcessType::IngressFeed, key.0).await?;
+ Ok(key)
}
#[cfg(feature = "cli")]
@@ -25,183 +27,227 @@ const CLI_TOTAL_INGRESS: u64 = 1000;
#[onelauncher_debug::debugger]
pub async fn init_ingress_internal(
- ingress_type: IngressType,
- total: f64,
- title: &str,
+ ingress_type: IngressType,
+ total: f64,
+ title: &str,
) -> crate::Result {
- let proxy_state = crate::ProxyState::get().await?;
- let key = IngressId(Uuid::new_v4());
+ let proxy_state = crate::ProxyState::get().await?;
+ let key = IngressId(Uuid::new_v4());
- proxy_state.ingress_feeds.write().await.insert(key.0, Ingress {
- ingress_uuid: key.0,
- message: title.to_string(),
- total,
- current: 0.0,
- last_sent: 0.0,
- ingress_type,
- #[cfg(feature = "cli")]
- cli: {
- let pb = indicatif::ProgressBar::new(CLI_TOTAL_INGRESS);
- pb.set_position(0);
- pb.set_style(indicatif::ProgressStyle::default_bar().template(
+ proxy_state.ingress_feeds.write().await.insert(
+ key.0,
+ Ingress {
+ ingress_uuid: key.0,
+ message: title.to_string(),
+ total,
+ current: 0.0,
+ last_sent: 0.0,
+ ingress_type,
+ #[cfg(feature = "cli")]
+ cli: {
+ let pb = indicatif::ProgressBar::new(CLI_TOTAL_INGRESS);
+ pb.set_position(0);
+ pb.set_style(indicatif::ProgressStyle::default_bar().template(
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}"
).unwrap().progress_chars("#>-"));
- pb
- },
- });
+ pb
+ },
+ },
+ );
- send_ingress(&key, 0.0, None).await?;
- Ok(key)
+ send_ingress(&key, 0.0, None).await?;
+ Ok(key)
}
pub async fn init_or_edit_ingress(
- id: Option,
- ingress_type: IngressType,
- total: f64,
- title: &str,
+ id: Option,
+ ingress_type: IngressType,
+ total: f64,
+ title: &str,
) -> crate::Result {
- if let Some(id) = id {
- edit_ingress(&id, ingress_type, total, title).await?;
+ if let Some(id) = id {
+ edit_ingress(&id, ingress_type, total, title).await?;
- Ok(id)
- } else {
- init_ingress(ingress_type, total, title).await
- }
+ Ok(id)
+ } else {
+ init_ingress(ingress_type, total, title).await
+ }
}
pub async fn edit_ingress(
- id: &IngressId,
- ingress_type: IngressType,
- total: f64,
- title: &str,
+ id: &IngressId,
+ ingress_type: IngressType,
+ total: f64,
+ title: &str,
) -> crate::Result<()> {
- let proxy_state = crate::ProxyState::get().await?;
+ let proxy_state = crate::ProxyState::get().await?;
- if let Some(ingress) = proxy_state.ingress_feeds.write().await.get_mut(&id.0) {
- ingress.ingress_type = ingress_type;
- ingress.total = total;
- ingress.message = title.to_string();
- ingress.current = 0.0;
- ingress.last_sent = 0.0;
- #[cfg(feature = "cli")]
- { ingress.cli.reset(); }
- };
+ if let Some(ingress) = proxy_state.ingress_feeds.write().await.get_mut(&id.0) {
+ ingress.ingress_type = ingress_type;
+ ingress.total = total;
+ ingress.message = title.to_string();
+ ingress.current = 0.0;
+ ingress.last_sent = 0.0;
+ #[cfg(feature = "cli")]
+ {
+ ingress.cli.reset();
+ }
+ };
- send_ingress(id, 0.0, None).await?;
- Ok(())
+ send_ingress(id, 0.0, None).await?;
+ Ok(())
}
#[allow(unused_variables)]
#[tracing::instrument(level = "debug")]
#[onelauncher_debug::debugger]
pub async fn send_ingress(
- key: &IngressId,
- increment: f64,
- message: Option<&str>,
+ key: &IngressId,
+ increment: f64,
+ message: Option<&str>,
) -> crate::Result<()> {
- let proxy_state = crate::ProxyState::get().await?;
- let mut ingress = proxy_state.ingress_feeds.write().await;
- let ingress = match ingress.get_mut(&key.0) {
- Some(f) => f,
- None => {
- return Err(ProxyError::NoIngressFound(key.0).into());
- }
- };
+ let proxy_state = crate::ProxyState::get().await?;
+ let mut ingress = proxy_state.ingress_feeds.write().await;
+ let ingress = match ingress.get_mut(&key.0) {
+ Some(f) => f,
+ None => {
+ return Err(ProxyError::NoIngressFound(key.0).into());
+ }
+ };
- ingress.current += increment;
- let display = ingress.current / ingress.total;
- let display_conv = if display >= 1.0 {
- None
- } else {
- Some(display)
- };
+ ingress.current += increment;
+ let display = ingress.current / ingress.total;
+ let display_conv = if display >= 1.0 { None } else { Some(display) };
- if f64::abs(display - ingress.last_sent) > 0.005 {
- #[cfg(feature = "cli")]
- {
- ingress.cli.set_message(message.map(|x| x.to_string()).unwrap_or(ingress.message.clone()));
- ingress.cli.set_position((display * CLI_TOTAL_INGRESS as f64).round() as u64);
- }
+ if f64::abs(display - ingress.last_sent) > 0.005 {
+ #[cfg(feature = "cli")]
+ {
+ ingress.cli.set_message(
+ message
+ .map(|x| x.to_string())
+ .unwrap_or(ingress.message.clone()),
+ );
+ ingress
+ .cli
+ .set_position((display * CLI_TOTAL_INGRESS as f64).round() as u64);
+ }
- #[cfg(feature = "tauri")]
- proxy_state.app.emit("ingress", IngressPayload {
- fraction: display_conv,
- message: message.unwrap_or(&ingress.message).to_string(),
- event: ingress.ingress_type.clone(),
- ingress_uuid: ingress.ingress_uuid,
- }).map_err(ProxyError::from)?;
+ #[cfg(feature = "tauri")]
+ proxy_state
+ .app
+ .emit(
+ "ingress",
+ IngressPayload {
+ fraction: display_conv,
+ message: message.unwrap_or(&ingress.message).to_string(),
+ event: ingress.ingress_type.clone(),
+ ingress_uuid: ingress.ingress_uuid,
+ },
+ )
+ .map_err(ProxyError::from)?;
- ingress.last_sent = display;
- }
+ ingress.last_sent = display;
+ }
- Ok(())
+ Ok(())
}
pub async fn send_message(message: &str) -> crate::Result<()> {
- #[cfg(feature = "tauri")]
- {
- let proxy_state = crate::ProxyState::get().await?;
- proxy_state.app.emit("message", MessagePayload { message: message.to_string() }).map_err(ProxyError::from)?;
- }
+ #[cfg(feature = "tauri")]
+ {
+ let proxy_state = crate::ProxyState::get().await?;
+ proxy_state
+ .app
+ .emit(
+ "message",
+ MessagePayload {
+ message: message.to_string(),
+ },
+ )
+ .map_err(ProxyError::from)?;
+ }
- tracing::warn!("{}", message);
- Ok(())
+ tracing::warn!("{}", message);
+ Ok(())
}
pub async fn send_offline(offline: bool) -> crate::Result<()> {
- #[cfg(feature = "tauri")]
- {
- let proxy_state = crate::ProxyState::get().await?;
- proxy_state.app.emit("offline", offline).map_err(ProxyError::from)?;
- }
+ #[cfg(feature = "tauri")]
+ {
+ let proxy_state = crate::ProxyState::get().await?;
+ proxy_state
+ .app
+ .emit("offline", offline)
+ .map_err(ProxyError::from)?;
+ }
- Ok(())
+ Ok(())
}
pub async fn send_internet(internet: InternetPayload) -> crate::Result<()> {
- tracing::debug!("operation {}", serde_json::to_string(&internet)?);
- #[cfg(feature = "tauri")]
- {
- let proxy_state = crate::ProxyState::get().await?;
- proxy_state.app.emit("internet", internet).map_err(ProxyError::from)?;
- }
+ tracing::debug!("operation {}", serde_json::to_string(&internet)?);
+ #[cfg(feature = "tauri")]
+ {
+ let proxy_state = crate::ProxyState::get().await?;
+ proxy_state
+ .app
+ .emit("internet", internet)
+ .map_err(ProxyError::from)?;
+ }
- Ok(())
+ Ok(())
}
pub async fn send_process(
- uuid: Uuid,
- pid: u32,
- event: ProcessPayloadType,
- message: &str,
+ uuid: Uuid,
+ pid: u32,
+ event: ProcessPayloadType,
+ message: &str,
) -> crate::Result<()> {
- #[cfg(feature = "tauri")]
- {
- let proxy_state = crate::ProxyState::get().await?;
- proxy_state.app.emit("process", ProcessPayload { uuid, pid, event, message: message.to_string() }).map_err(ProxyError::from)?;
- }
+ #[cfg(feature = "tauri")]
+ {
+ let proxy_state = crate::ProxyState::get().await?;
+ proxy_state
+ .app
+ .emit(
+ "process",
+ ProcessPayload {
+ uuid,
+ pid,
+ event,
+ message: message.to_string(),
+ },
+ )
+ .map_err(ProxyError::from)?;
+ }
- Ok(())
+ Ok(())
}
pub async fn send_cluster(
- uuid: Uuid,
- cluster_path: &ClusterPath,
- name: &str,
- event: ClusterPayloadType,
+ uuid: Uuid,
+ cluster_path: &ClusterPath,
+ name: &str,
+ event: ClusterPayloadType,
) -> crate::Result<()> {
- #[cfg(feature = "tauri")]
- {
- let path = cluster_path.full_path().await?;
- let proxy_state = crate::ProxyState::get().await?;
- proxy_state.app.emit("cluster", ClusterPayload {
- uuid,
- cluster_path: cluster_path.clone(),
- path,
- name: name.to_string(),
- event,
- }).map_err(ProxyError::from)?;
- }
+ #[cfg(feature = "tauri")]
+ {
+ let path = cluster_path.full_path().await?;
+ let proxy_state = crate::ProxyState::get().await?;
+ proxy_state
+ .app
+ .emit(
+ "cluster",
+ ClusterPayload {
+ uuid,
+ cluster_path: cluster_path.clone(),
+ path,
+ name: name.to_string(),
+ event,
+ },
+ )
+ .map_err(ProxyError::from)?;
+ }
- Ok(())
+ Ok(())
}
diff --git a/core/src/api/proxy/utils.rs b/core/src/api/proxy/utils.rs
index db70fc2e..7463b8e4 100644
--- a/core/src/api/proxy/utils.rs
+++ b/core/src/api/proxy/utils.rs
@@ -47,31 +47,32 @@ macro_rules! ingress_join {
#[tracing::instrument(skip(stream, f))]
#[onelauncher_debug::debugger]
pub async fn ingress_try_for_each(
- stream: I,
- limit: Option,
- key: Option<&IngressId>,
- total: f64,
- num_futs: usize,
- message: Option<&str>,
- f: F,
+ stream: I,
+ limit: Option,
+ key: Option<&IngressId>,
+ total: f64,
+ num_futs: usize,
+ message: Option<&str>,
+ f: F,
) -> crate::Result<()>
where
- I: futures::TryStreamExt + TryStream,
- F: FnMut(T) -> Fut + Send,
- Fut: Future> + Send,
- T: Send,
+ I: futures::TryStreamExt + TryStream,
+ F: FnMut(T) -> Fut + Send,
+ Fut: Future> + Send,
+ T: Send,
{
- let mut f = f;
- stream.try_for_each_concurrent(limit, |item| {
- let f = f(item);
- async move {
- f.await?;
- if let Some(key) = key {
- send_ingress(key, total / (num_futs as f64), message).await?;
- }
+ let mut f = f;
+ stream
+ .try_for_each_concurrent(limit, |item| {
+ let f = f(item);
+ async move {
+ f.await?;
+ if let Some(key) = key {
+ send_ingress(key, total / (num_futs as f64), message).await?;
+ }
- Ok(())
- }
- })
- .await
+ Ok(())
+ }
+ })
+ .await
}
diff --git a/core/src/api/settings.rs b/core/src/api/settings.rs
index 66c6e10b..c4d6b913 100644
--- a/core/src/api/settings.rs
+++ b/core/src/api/settings.rs
@@ -1,180 +1,201 @@
//! Settings Management
-use std::path::{Path, PathBuf};
-use tokio::sync::RwLock;
use crate::proxy::send::{init_ingress, send_ingress};
use crate::store::{self, Clusters, Directories, Settings};
use crate::State;
+use std::path::{Path, PathBuf};
+use tokio::sync::RwLock;
/// gets a [`Settings`] object state.
#[tracing::instrument]
pub async fn get() -> crate::Result {
- let state = State::get().await?;
- let settings = state.settings.read().await;
- Ok(settings.clone())
+ let state = State::get().await?;
+ let settings = state.settings.read().await;
+ Ok(settings.clone())
}
/// sets a [`Settings`] object state.
#[tracing::instrument]
pub async fn set(settings: Settings) -> crate::Result<()> {
- let state = State::get().await?;
+ let state = State::get().await?;
- if settings.config_dir != state.settings.read().await.config_dir {
- return Err(anyhow::anyhow!("ccannot change config directory as a setting").into());
- }
+ if settings.config_dir != state.settings.read().await.config_dir {
+ return Err(anyhow::anyhow!("ccannot change config directory as a setting").into());
+ }
- let (io, fetch) = async {
- let read = state.settings.read().await;
- (
- settings.max_async_io_operations != read.max_async_io_operations,
- settings.max_async_fetches != read.max_async_fetches,
- )
- }.await;
+ let (io, fetch) = async {
+ let read = state.settings.read().await;
+ (
+ settings.max_async_io_operations != read.max_async_io_operations,
+ settings.max_async_fetches != read.max_async_fetches,
+ )
+ }
+ .await;
- let discord_rpc = {
- let read = state.settings.read().await;
- settings.disable_discord != read.disable_discord
- };
+ let discord_rpc = {
+ let read = state.settings.read().await;
+ settings.disable_discord != read.disable_discord
+ };
- {
- *state.settings.write().await = settings;
- }
+ {
+ *state.settings.write().await = settings;
+ }
- if discord_rpc {
- state.discord_rpc.clear(true).await?;
- }
+ if discord_rpc {
+ state.discord_rpc.clear(true).await?;
+ }
- if io {
- state.reset_io_semaphore().await;
- }
+ if io {
+ state.reset_io_semaphore().await;
+ }
- if fetch {
- state.reset_fetch_semaphore().await;
- }
+ if fetch {
+ state.reset_fetch_semaphore().await;
+ }
- State::sync().await?;
- Ok(())
+ State::sync().await?;
+ Ok(())
}
/// sets the config and caches directory, this can have side effects.
pub async fn set_directory(new: PathBuf) -> crate::Result<()> {
- tracing::trace!("changing config directory to {}", new.display());
+ tracing::trace!("changing config directory to {}", new.display());
- if !new.is_dir() {
- return Err(anyhow::anyhow!("new config directory {} is not a folder!", new.display()).into());
- }
+ if !new.is_dir() {
+ return Err(
+ anyhow::anyhow!("new config directory {} is not a folder!", new.display()).into(),
+ );
+ }
- if !is_writable(new.clone()).await? {
- return Err(anyhow::anyhow!("new config directory {} is not writeable", new.display()).into());
- }
+ if !is_writable(new.clone()).await? {
+ return Err(
+ anyhow::anyhow!("new config directory {} is not writeable", new.display()).into(),
+ );
+ }
- let ingress = init_ingress(
- crate::IngressType::SyncConfig { new_path: new.clone() },
- 100.0, "syncing and changing configuration directory",
- ).await?;
+ let ingress = init_ingress(
+ crate::IngressType::SyncConfig {
+ new_path: new.clone(),
+ },
+ 100.0,
+ "syncing and changing configuration directory",
+ )
+ .await?;
- tracing::trace!("changing config directory, overriding state write access");
- let mut state_write = State::get_and_write().await?;
- let old = state_write.directories.config_dir.read().await.clone();
+ tracing::trace!("changing config directory, overriding state write access");
+ let mut state_write = State::get_and_write().await?;
+ let old = state_write.directories.config_dir.read().await.clone();
- tracing::trace!("resetting file watcher after changing config directory");
- let fs_watcher = crate::utils::watcher::initialize_watcher().await?;
- state_write.watcher = RwLock::new(fs_watcher);
+ tracing::trace!("resetting file watcher after changing config directory");
+ let fs_watcher = crate::utils::watcher::initialize_watcher().await?;
+ state_write.watcher = RwLock::new(fs_watcher);
- tracing::trace!("collecting all config files to be transfered");
- let mut cfg_entries = crate::utils::io::read_dir(&old).await?;
- let across_filesystems = is_different_fs(&old, &new);
- let mut entries = vec![];
- let mut cleanable = vec![];
+ tracing::trace!("collecting all config files to be transfered");
+ let mut cfg_entries = crate::utils::io::read_dir(&old).await?;
+ let across_filesystems = is_different_fs(&old, &new);
+ let mut entries = vec![];
+ let mut cleanable = vec![];
- while let Some(e) = cfg_entries.next_entry().await.map_err(|err| crate::utils::io::IOError::with_path(err, &old))? {
- let epath = e.path();
- if let Some(file_name) = epath.file_name() {
- if file_name == store::CLUSTERS_FOLDER
- || file_name == store::METADATA_FOLDER
- {
- if across_filesystems {
- entries.extend(crate::package::import::sub(&epath).await?);
- cleanable.push(epath.clone());
- } else {
- entries.push(epath.clone());
- }
- }
- }
- }
+ while let Some(e) = cfg_entries
+ .next_entry()
+ .await
+ .map_err(|err| crate::utils::io::IOError::with_path(err, &old))?
+ {
+ let epath = e.path();
+ if let Some(file_name) = epath.file_name() {
+ if file_name == store::CLUSTERS_FOLDER || file_name == store::METADATA_FOLDER {
+ if across_filesystems {
+ entries.extend(crate::package::import::sub(&epath).await?);
+ cleanable.push(epath.clone());
+ } else {
+ entries.push(epath.clone());
+ }
+ }
+ }
+ }
- tracing::trace!("transferring configuration files");
- let io_semaphore = &state_write.io_semaphore;
- let nentries = entries.len() as f64;
- for epath in entries {
- let relpath = epath.strip_prefix(&old).map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?;
- let newpath = new.join(relpath);
- if across_filesystems {
- crate::utils::http::copy(&epath, &newpath, io_semaphore).await?;
- } else {
- crate::utils::io::rename(epath.clone(), newpath.clone()).await?;
- }
+ tracing::trace!("transferring configuration files");
+ let io_semaphore = &state_write.io_semaphore;
+ let nentries = entries.len() as f64;
+ for epath in entries {
+ let relpath = epath
+ .strip_prefix(&old)
+ .map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?;
+ let newpath = new.join(relpath);
+ if across_filesystems {
+ crate::utils::http::copy(&epath, &newpath, io_semaphore).await?;
+ } else {
+ crate::utils::io::rename(epath.clone(), newpath.clone()).await?;
+ }
- send_ingress(&ingress, 80.0 * (1.0 / nentries), None).await?;
- }
+ send_ingress(&ingress, 80.0 * (1.0 / nentries), None).await?;
+ }
- tracing::trace!("re-setting configuration settings");
- let settings = {
- let mut settings = state_write.settings.write().await;
- settings.config_dir = Some(new.clone());
+ tracing::trace!("re-setting configuration settings");
+ let settings = {
+ let mut settings = state_write.settings.write().await;
+ settings.config_dir = Some(new.clone());
- tracing::trace!("updating java paths and keys");
- for j in settings.java_versions.keys() {
- if let Some(java) = settings.java_versions.get_mut(&j) {
- if let Ok(relpath) = PathBuf::from(java.path.clone()).strip_prefix(&old) {
- java.path = new.join(relpath).to_string_lossy().to_string();
- }
- }
- }
+ tracing::trace!("updating java paths and keys");
+ for j in settings.java_versions.keys() {
+ if let Some(java) = settings.java_versions.get_mut(&j) {
+ if let Ok(relpath) = PathBuf::from(java.path.clone()).strip_prefix(&old) {
+ java.path = new.join(relpath).to_string_lossy().to_string();
+ }
+ }
+ }
- tracing::trace!("re-syncing settings file");
- settings.sync(&state_write.directories.settings_file()).await?;
- settings.clone()
- };
+ tracing::trace!("re-syncing settings file");
+ settings
+ .sync(&state_write.directories.settings_file())
+ .await?;
+ settings.clone()
+ };
- tracing::trace!("re-initializing the config directory");
- state_write.directories = Directories::initalize(&settings)?;
- let cleanable_nentries = cleanable.len();
- if cleanable_nentries > 0 { tracing::trace!("deleting old config files"); }
- for ce in cleanable {
- crate::utils::io::remove_dir_all(ce).await?;
- send_ingress(&ingress, 10.0 * (1.0 / cleanable_nentries as f64), None).await?;
- }
+ tracing::trace!("re-initializing the config directory");
+ state_write.directories = Directories::initalize(&settings)?;
+ let cleanable_nentries = cleanable.len();
+ if cleanable_nentries > 0 {
+ tracing::trace!("deleting old config files");
+ }
+ for ce in cleanable {
+ crate::utils::io::remove_dir_all(ce).await?;
+ send_ingress(&ingress, 10.0 * (1.0 / cleanable_nentries as f64), None).await?;
+ }
- tracing::trace!("re-resetting file watching system");
- let mut file_watcher = crate::utils::watcher::initialize_watcher().await?;
- state_write.clusters = RwLock::new(Clusters::initialize(&state_write.directories, &mut file_watcher).await?);
- state_write.watcher = RwLock::new(file_watcher);
- send_ingress(&ingress, 10.0, None).await?;
-
- tracing::info!("successfully switched configuration directory to {}", new.display());
+ tracing::trace!("re-resetting file watching system");
+ let mut file_watcher = crate::utils::watcher::initialize_watcher().await?;
+ state_write.clusters =
+ RwLock::new(Clusters::initialize(&state_write.directories, &mut file_watcher).await?);
+ state_write.watcher = RwLock::new(file_watcher);
+ send_ingress(&ingress, 10.0, None).await?;
- Ok(())
+ tracing::info!(
+ "successfully switched configuration directory to {}",
+ new.display()
+ );
+
+ Ok(())
}
/// checks if two paths are on different data storage devices
fn is_different_fs(a: &Path, b: &Path) -> bool {
- let roota = a.components().next();
- let rootb = b.components().next();
- roota != rootb
+ let roota = a.components().next();
+ let rootb = b.components().next();
+ roota != rootb
}
/// checks if a [`PathBuf`] is writable or locked.
pub async fn is_writable(path: PathBuf) -> crate::Result {
- let tmp = path.join(".tmp");
- match crate::utils::io::write(tmp.clone(), "test").await {
- Ok(_) => {
- crate::utils::io::remove_file(tmp).await?;
- Ok(true)
- }
- Err(e) => {
- tracing::error!("failed to write to config directory: {}", e);
- Ok(false)
- }
- }
+ let tmp = path.join(".tmp");
+ match crate::utils::io::write(tmp.clone(), "test").await {
+ Ok(_) => {
+ crate::utils::io::remove_file(tmp).await?;
+ Ok(true)
+ }
+ Err(e) => {
+ tracing::error!("failed to write to config directory: {}", e);
+ Ok(false)
+ }
+ }
}
diff --git a/core/src/constants.rs b/core/src/constants.rs
index dd0abb2f..32830f0a 100644
--- a/core/src/constants.rs
+++ b/core/src/constants.rs
@@ -1,5 +1,5 @@
//! **OneLauncher Constants**
-//!
+//!
//! Public constant variables and strings that are used throughout the launcher.
//! Can be shared publically, any encrypted secrets should be stored in [`crate::store::credentials`].
diff --git a/core/src/error.rs b/core/src/error.rs
index 53b78e8a..e41607dc 100644
--- a/core/src/error.rs
+++ b/core/src/error.rs
@@ -1,5 +1,5 @@
//! **OneLauncher Errors**
-//!
+//!
//! Public wrappers around common OneLauncher errors.
use tracing_error::InstrumentError;
@@ -75,10 +75,18 @@ pub enum ErrorKind {
#[error("failed to authenticate with microsoft: {0}")]
AuthError(#[from] crate::store::MinecraftAuthError),
+ /// Wrapper around [`crate::api::cluster::create::CreateClusterError`] to handle Cluster creation errors.
+ #[error("failed to create clusters: {0}")]
+ CreateClusterError(#[from] crate::api::cluster::create::CreateClusterError),
+
/// Wrapper around [`notify:Error`] to handle file watching errors.
#[error("failed to watch file {0}")]
NotifyError(#[from] notify::Error),
+ /// Wrapper around [`regex::Error`] to handle RegExp errors.
+ #[error("string verification with regex failed: {0}")]
+ RegexError(#[from] regex::Error),
+
/// Wrapper around [`reqwest::header::ToStrError`] to handle header conversion errors.
#[error("failed to convert header to string: {0}")]
HeaderToStrError(#[from] reqwest::header::ToStrError),
@@ -97,7 +105,7 @@ pub enum ErrorKind {
/// Wrapper around [`std::io::Error`] to handle non-wrapped std::io errors.
#[error("I/O (std) error: {0}")]
- StdIOError(#[from] std::io::Error),
+ StdIOError(#[from] std::io::Error),
/// Wrapper around [`crate::utils::java::JavaError`] to handle java errors.
#[error("there was an error managing java installations: {0}")]
@@ -109,8 +117,8 @@ pub enum ErrorKind {
/// Wrapper around [`tauri::Error`] to handle Tauri errors when the feature flag is enabled
#[cfg(feature = "tauri")]
- #[error("Tauri error: {0}")]
- TauriError(#[from] tauri::Error),
+ #[error("Tauri error: {0}")]
+ TauriError(#[from] tauri::Error),
}
#[derive(Debug)]
diff --git a/core/src/game/arguments.rs b/core/src/game/arguments.rs
new file mode 100644
index 00000000..f33c800a
--- /dev/null
+++ b/core/src/game/arguments.rs
@@ -0,0 +1,403 @@
+//! Programatic Minecraft argument parser which replaces variables in arguments
+
+use std::collections::{HashMap, HashSet};
+use std::io::prelude::BufRead;
+use std::io::BufReader;
+use std::path::Path;
+
+use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
+use interpulse::api::modded::SidedDataEntry;
+use interpulse::utils::get_path_from_artifact;
+
+use crate::store::{Memory, MinecraftCredentials, Resolution};
+use crate::utils::io::IOError;
+use crate::utils::platform::classpath_separator;
+
+const DUMMY_REPLACE_NEWLINE: &str = "\n";
+
+#[allow(clippy::too_many_arguments)]
+pub fn java_arguments(
+ arguments: Option<&[Argument]>,
+ natives_path: &Path,
+ libraries_path: &Path,
+ classpaths: &str,
+ version: &str,
+ memory: Memory,
+ custom_args: Vec,
+ java_arch: &str,
+) -> crate::Result> {
+ let mut parsed = Vec::new();
+ if let Some(args) = arguments {
+ parse_arguments(
+ args,
+ &mut parsed,
+ |a| {
+ parse_java_argument(
+ a.to_string(),
+ natives_path,
+ libraries_path,
+ classpaths,
+ version,
+ java_arch,
+ )
+ },
+ java_arch,
+ )?;
+ } else {
+ parsed.push(format!(
+ "-Djava.library.path={}",
+ dunce::canonicalize(natives_path)
+ .map_err(|_| anyhow::anyhow!(
+ "specified natives path {} not found",
+ natives_path.to_string_lossy()
+ ))?
+ .to_string_lossy()
+ ));
+ parsed.push("-cp".to_string());
+ parsed.push(classpaths.to_string());
+ }
+
+ parsed.push(format!("-Xmx{}M", memory.maximum));
+ parsed.push(format!("-Xms{}M", memory.minimum));
+
+ for arg in custom_args {
+ if !arg.is_empty() {
+ parsed.push(arg);
+ }
+ }
+
+ Ok(parsed)
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn minecraft_arguments(
+ args: Option<&[Argument]>,
+ legacy_args: Option<&str>,
+ creds: &MinecraftCredentials,
+ version: &str,
+ asset_index: &str,
+ game_directory: &Path,
+ assets_directory: &Path,
+ version_type: &VersionType,
+ resolution: Resolution,
+ java_arch: &str,
+) -> crate::Result> {
+ if let Some(args) = args {
+ let mut parsed = Vec::new();
+ parse_arguments(
+ args,
+ &mut parsed,
+ |arg| {
+ parse_minecraft_argument(
+ arg,
+ &creds.access_token,
+ &creds.username,
+ &creds.id,
+ version,
+ asset_index,
+ game_directory,
+ assets_directory,
+ version_type,
+ resolution,
+ )
+ },
+ java_arch,
+ )?;
+
+ Ok(parsed)
+ } else if let Some(legacy_args) = legacy_args {
+ let mut parsed = Vec::new();
+ for arg in legacy_args.split(' ') {
+ parsed.push(parse_minecraft_argument(
+ &arg.replace(' ', DUMMY_REPLACE_NEWLINE),
+ &creds.access_token,
+ &creds.username,
+ &creds.id,
+ version,
+ asset_index,
+ game_directory,
+ assets_directory,
+ version_type,
+ resolution,
+ )?);
+ }
+
+ Ok(parsed)
+ } else {
+ Ok(Vec::new())
+ }
+}
+
+pub fn processor_arguments>(
+ libraries_path: &Path,
+ args: &[T],
+ data: &HashMap,
+) -> crate::Result> {
+ let mut parsed = Vec::new();
+
+ for arg in args {
+ let a = &arg.as_ref()[1..arg.as_ref().len() - 1];
+ if arg.as_ref().starts_with('{') {
+ if let Some(entry) = data.get(a) {
+ parsed.push(if entry.client.starts_with('[') {
+ get_library(
+ libraries_path,
+ &entry.client[1..entry.client.len() - 1],
+ true,
+ )?
+ } else {
+ entry.client.clone()
+ })
+ }
+ } else if arg.as_ref().starts_with('[') {
+ parsed.push(get_library(libraries_path, a, true)?)
+ } else {
+ parsed.push(arg.as_ref().to_string())
+ }
+ }
+
+ Ok(parsed)
+}
+
+pub async fn main_class(path: String) -> crate::Result> {
+ let main_class = tokio::task::spawn_blocking(move || {
+ let zipfs = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?;
+ let mut archive = zip::ZipArchive::new(zipfs)
+ .map_err(|_| anyhow::anyhow!("failed to read main class {}", path))?;
+ let file = archive
+ .by_name("META-INF/MANIFEST.MF")
+ .map_err(|_| anyhow::anyhow!("failed to read manifest {}", path))?;
+
+ let zip_reader = BufReader::new(file);
+ for line in zip_reader.lines() {
+ let mut line = line.map_err(IOError::from)?;
+ line.retain(|c| !c.is_whitespace());
+
+ if line.starts_with("Main-Class:") {
+ if let Some(class) = line.split(':').nth(1) {
+ return Ok(Some(class.to_string()));
+ }
+ }
+ }
+
+ Ok:: , crate::Error>(None)
+ })
+ .await??;
+
+ Ok(main_class)
+}
+
+#[allow(clippy::too_many_arguments)]
+fn parse_minecraft_argument(
+ argument: &str,
+ access_token: &str,
+ username: &str,
+ uuid: &uuid::Uuid,
+ version: &str,
+ asset_index: &str,
+ game_directory: &Path,
+ assets_directory: &Path,
+ version_type: &VersionType,
+ resolution: Resolution,
+) -> crate::Result {
+ Ok(argument
+ .replace("${accessToken}", access_token)
+ .replace("${auth_access_token}", access_token)
+ .replace("${auth_session}", access_token)
+ .replace("${auth_player_name}", username)
+ .replace("${auth_xuid}", "10")
+ .replace("${auth_uuid}", &uuid.simple().to_string())
+ .replace("${client_id}", crate::constants::CLIENT_ID)
+ .replace("${user_properties}", "{}")
+ .replace("${user_type", "msa")
+ .replace("${version_name}", version)
+ .replace("${asset_index_name}", asset_index)
+ .replace(
+ "${game_directory}",
+ &dunce::canonicalize(game_directory)
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "game directory {} doesn't exist",
+ game_directory.to_string_lossy()
+ )
+ })?
+ .to_string_lossy(),
+ )
+ .replace(
+ "${assets_root}",
+ &dunce::canonicalize(assets_directory)
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "assets directory {} doesn't exist",
+ assets_directory.to_string_lossy()
+ )
+ })?
+ .to_string_lossy(),
+ )
+ .replace(
+ "${game_assets}",
+ &dunce::canonicalize(assets_directory)
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "assets directory {} doesn't exist",
+ assets_directory.to_string_lossy()
+ )
+ })?
+ .to_string_lossy(),
+ )
+ .replace("${version_type}", version_type.as_str())
+ .replace("${resolution_width}", &resolution.0.to_string())
+ .replace("${resolution_height}", &resolution.1.to_string()))
+}
+
+#[allow(clippy::too_many_arguments)]
+fn parse_java_argument(
+ mut argument: String,
+ natives_path: &Path,
+ libraries_path: &Path,
+ classpaths: &str,
+ version: &str,
+ java_arch: &str,
+) -> crate::Result {
+ argument.retain(|c| !c.is_whitespace());
+ Ok(argument
+ .replace(
+ "${natives_directory}",
+ &dunce::canonicalize(natives_path)
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "natives path {} doesn't exist",
+ natives_path.to_string_lossy()
+ )
+ })?
+ .to_string_lossy(),
+ )
+ .replace(
+ "${library_directory}",
+ &dunce::canonicalize(libraries_path)
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "libraries path {} doesn't eixst",
+ libraries_path.to_string_lossy()
+ )
+ })?
+ .to_string_lossy(),
+ )
+ .replace("${classpath_seperator}", classpath_separator(java_arch))
+ .replace("${launcher_name}", "onelauncher")
+ .replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
+ .replace("${version_name}", version)
+ .replace("${classpath}", classpaths))
+}
+
+pub fn classpaths(
+ libraries_path: &Path,
+ libraries: &[Library],
+ client_path: &Path,
+ java_arch: &str,
+ updated: bool,
+) -> crate::Result {
+ let mut classpaths = libraries
+ .iter()
+ .filter_map(|lib| {
+ if let Some(rules) = &lib.rules {
+ if !crate::game::rules(rules, java_arch, updated) {
+ return None;
+ }
+ }
+
+ if !lib.include_in_classpath {
+ return None;
+ }
+
+ Some(get_library(libraries_path, &lib.name, false))
+ })
+ .collect::, _>>()?;
+
+ classpaths.insert(
+ dunce::canonicalize(client_path)
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "specified classpath {} not found",
+ client_path.to_string_lossy()
+ )
+ })?
+ .to_string_lossy()
+ .to_string(),
+ );
+
+ Ok(classpaths
+ .into_iter()
+ .collect::>()
+ .join(classpath_separator(java_arch)))
+}
+
+pub fn get_classpath_library>(
+ libraries_path: &Path,
+ libraries: &[T],
+ java_arch: &str,
+) -> crate::Result {
+ let classpaths = libraries
+ .iter()
+ .map(|lib| get_library(libraries_path, lib.as_ref(), false))
+ .collect::, _>>()?;
+
+ Ok(classpaths.join(classpath_separator(java_arch)))
+}
+
+pub fn get_library(
+ libraries_path: &Path,
+ library: &str,
+ error_exist: bool,
+) -> crate::Result {
+ let mut path = libraries_path.to_path_buf();
+ path.push(get_path_from_artifact(library)?);
+
+ if !path.exists() && error_exist {
+ return Ok(path.to_string_lossy().to_string());
+ }
+
+ let path = &dunce::canonicalize(&path)
+ .map_err(|_| anyhow::anyhow!("library file {} not found", path.to_string_lossy()))?;
+
+ Ok(path.to_string_lossy().to_string())
+}
+
+fn parse_arguments(
+ args: &[Argument],
+ parsed: &mut Vec,
+ parse_function: ParseFn,
+ java_arch: &str,
+) -> crate::Result<()>
+where
+ ParseFn: Fn(&str) -> crate::Result,
+{
+ for arg in args {
+ match arg {
+ Argument::Normal(a) => {
+ let p = parse_function(&a.replace(' ', DUMMY_REPLACE_NEWLINE))?;
+ for split_p in p.split(DUMMY_REPLACE_NEWLINE) {
+ parsed.push(split_p.to_string());
+ }
+ }
+ Argument::Ruled { rules, value } => {
+ if super::rules(rules, java_arch, true) {
+ match value {
+ ArgumentValue::Single(arg) => {
+ parsed.push(parse_function(&arg.replace(' ', DUMMY_REPLACE_NEWLINE))?);
+ }
+ ArgumentValue::Many(args) => {
+ for arg in args {
+ parsed.push(parse_function(
+ &arg.replace(' ', DUMMY_REPLACE_NEWLINE),
+ )?);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
diff --git a/core/src/game/metadata.rs b/core/src/game/metadata.rs
new file mode 100644
index 00000000..cccf2f46
--- /dev/null
+++ b/core/src/game/metadata.rs
@@ -0,0 +1,328 @@
+//! Downloads Minecraft metadata to be launched
+
+use futures::prelude::*;
+use interpulse as ip;
+use tokio::sync::OnceCell;
+
+use interpulse::api::minecraft::{
+ Asset, AssetsIndex, Library, Os, Version as GameVersion, VersionInfo as GameVersionInfo,
+};
+use interpulse::api::modded::LoaderVersion;
+
+use crate::proxy::send::send_ingress;
+use crate::proxy::utils::ingress_try_for_each;
+use crate::proxy::IngressId;
+use crate::store::State;
+use crate::utils::http::*;
+use crate::utils::io;
+use crate::utils::platform::OsExt;
+
+#[tracing::instrument(skip(st, version))]
+pub async fn download_minecraft(
+ st: &State,
+ version: &GameVersionInfo,
+ ingress: &IngressId,
+ java_arch: &str,
+ force: bool,
+ updated: bool,
+) -> crate::Result<()> {
+ tracing::info!("downloading minecraft version {}", version.id);
+ let assets_index = download_assets_index(st, version, Some(ingress), force).await?;
+ let processors = if version
+ .processors
+ .as_ref()
+ .map(|p| !p.is_empty())
+ .unwrap_or(false)
+ {
+ 25.0
+ } else {
+ 40.0
+ };
+
+ tokio::try_join! {
+ download_client(st, version, Some(ingress), force),
+ download_assets(st, version.assets == "legacy", &assets_index, Some(ingress), processors, force),
+ download_libraries(st, version.libraries.as_slice(), &version.id, Some(ingress), processors, java_arch, force, updated)
+ }?;
+
+ tracing::info!("downloaded minecraft version {}", version.id);
+ Ok(())
+}
+
+#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
+#[onelauncher_debug::debugger]
+pub async fn download_version_info(
+ st: &State,
+ version: &GameVersion,
+ loader: Option<&LoaderVersion>,
+ force: Option,
+ ingress: Option<&IngressId>,
+) -> crate::Result {
+ let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
+ tracing::debug!("loading minecraft version info for minecraft version {version_id}");
+ let path = st
+ .directories
+ .version_dir(&version_id)
+ .await
+ .join(format!("{version_id}.json"));
+ let result = if path.exists() && !force.unwrap_or(false) {
+ io::read(path)
+ .err_into::()
+ .await
+ .and_then(|ref it| Ok(serde_json::from_slice(it)?))
+ } else {
+ tracing::info!(
+ "downloading minecraft version info for minecraft version {}",
+ &version_id
+ );
+ let mut info = ip::api::minecraft::fetch_version_info(version).await?;
+
+ if let Some(loader) = loader {
+ let partial = ip::api::modded::fetch_partial_version(&loader.url).await?;
+ info = ip::api::modded::merge_partial_version(partial, info);
+ }
+
+ info.id = version_id.clone();
+
+ write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?;
+ Ok(info)
+ }?;
+
+ if let Some(ingress) = ingress {
+ send_ingress(ingress, 5.0, None).await?;
+ }
+
+ tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
+ Ok(result)
+}
+
+#[tracing::instrument(skip_all)]
+#[onelauncher_debug::debugger]
+pub async fn download_assets_index(
+ st: &State,
+ version: &GameVersionInfo,
+ ingress: Option<&IngressId>,
+ force: bool,
+) -> crate::Result {
+ tracing::debug!("loading assets index");
+ let path = st
+ .directories
+ .index_dir()
+ .await
+ .join(format!("{}.json", &version.asset_index.id));
+
+ let result = if path.exists() && !force {
+ io::read(path)
+ .err_into::()
+ .await
+ .and_then(|ref it| Ok(serde_json::from_slice(it)?))
+ } else {
+ let index = ip::api::minecraft::fetch_assets_index(version).await?;
+ write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?;
+ tracing::info!("downloaded assets index");
+ Ok(index)
+ }?;
+
+ if let Some(ingress) = ingress {
+ send_ingress(ingress, 5.0, None).await?;
+ }
+
+ tracing::debug!("loaded assets index");
+ Ok(result)
+}
+
+#[tracing::instrument(skip(st, index))]
+#[onelauncher_debug::debugger]
+pub async fn download_assets(
+ st: &State,
+ legacy: bool,
+ index: &AssetsIndex,
+ ingress: Option<&IngressId>,
+ ingress_amount: f64,
+ force: bool,
+) -> crate::Result<()> {
+ tracing::debug!("loading minecraft assets");
+ let num_futs = index.objects.len();
+ let assets = stream::iter(index.objects.iter()).map(Ok::<(&String, &Asset), crate::Error>);
+
+ ingress_try_for_each(
+ assets,
+ None,
+ ingress,
+ ingress_amount,
+ num_futs,
+ None,
+ |(name, asset)| async move {
+ let hash = &asset.hash;
+ let resources = st.directories.object_dir(hash).await;
+ let url = format!(
+ "https://resources.download.minecraft.net/{sub_hash}/{hash}",
+ sub_hash = &hash[..2]
+ );
+ let fetch_cell = OnceCell::::new();
+ tokio::try_join! {
+ async {
+ if !resources.exists() || force {
+ let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
+ write(&resources, resource, &st.io_semaphore).await?;
+ tracing::trace!("fetched asset resource with hash {hash}");
+ }
+ Ok::<_, crate::Error>(())
+ },
+ async {
+ let resources = st.directories.legacy_assets_dir().await.join(
+ name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
+ );
+
+ if legacy && !resources.exists() || force {
+ let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
+ write(&resources, resource, &st.io_semaphore).await?;
+ tracing::trace!("fetched legacy asset resource with hash {hash}");
+ }
+ Ok::<_, crate::Error>(())
+ },
+ }?;
+
+ tracing::trace!("loaded asset resource with hash {hash}");
+ Ok(())
+ },
+ )
+ .await?;
+
+ tracing::debug!("loaded minecraft assets");
+ Ok(())
+}
+
+#[tracing::instrument(skip(st, libraries))]
+#[onelauncher_debug::debugger]
+#[allow(clippy::too_many_arguments)]
+pub async fn download_libraries(
+ st: &State,
+ libraries: &[Library],
+ version: &str,
+ ingress: Option<&IngressId>,
+ ingress_amount: f64,
+ java_arch: &str,
+ force: bool,
+ updated: bool,
+) -> crate::Result<()> {
+ tracing::debug!("loading minecraft libraries");
+
+ tokio::try_join! {
+ io::create_dir_all(st.directories.libraries_dir().await),
+ io::create_dir_all(st.directories.version_natives_dir(version).await)
+ }?;
+
+ let num_files = libraries.len();
+ ingress_try_for_each(
+ stream::iter(libraries.iter())
+ .map(Ok::<&Library, crate::Error>),
+ None,
+ ingress,
+ ingress_amount,
+ num_files,
+ None,
+ |lib| async move {
+ if let Some(rules) = &lib.rules {
+ if !super::rules(rules, java_arch, updated) {
+ tracing::trace!("skipped library {} due to rule", &lib.name);
+ return Ok(());
+ }
+ }
+
+ tokio::try_join! {
+ async {
+ let artifact_path = ip::utils::get_path_from_artifact(&lib.name)?;
+ let path = st.directories.libraries_dir().await.join(&artifact_path);
+
+ match lib.downloads {
+ _ if path.exists() && !force => Ok(()),
+ Some(ip::api::minecraft::LibraryDownloads {
+ artifact: Some(ref artifact),
+ ..
+ }) => {
+ let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore).await?;
+ write(&path, &bytes, &st.io_semaphore).await?;
+ tracing::trace!("downloaded library {} into path {:?}", &lib.name, &path);
+ Ok::<_, crate::Error>(())
+ }
+ _ => {
+ let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
+ let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
+ write(&path, &bytes, &st.io_semaphore).await?;
+ tracing::trace!("downloaded library {} into path {:?}", &lib.name, &path);
+ Ok::<_, crate::Error>(())
+ }
+ }
+ },
+ async {
+ if let Some((os_key, classifiers)) = None.or_else(|| Some((
+ lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?,
+ lib.downloads.as_ref()?.classifiers.as_ref()?
+ ))) {
+ let parsed = os_key.replace("${arch}", crate::utils::platform::ARCH_WIDTH);
+ if let Some(native) = classifiers.get(&parsed) {
+ let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?;
+ let reader = std::io::Cursor::new(&data);
+ if let Ok(mut archive) = zip::ZipArchive::new(reader) {
+ match archive.extract(st.directories.version_natives_dir(version).await) {
+ Ok(_) => tracing::debug!("downloaded native {}", &lib.name),
+ Err(err) => tracing::error!("failed to download native {}: {}", &lib.name, err)
+ }
+ } else {
+ tracing::error!("failed to extract native {}", &lib.name)
+ }
+ }
+ }
+
+ Ok(())
+ }
+ }?;
+
+ tracing::debug!("loaded minecraft library {}", lib.name);
+ Ok(())
+ }
+ ).await?;
+
+ tracing::debug!("downloaded all minecraft libraries");
+ Ok(())
+}
+
+#[tracing::instrument(skip_all)]
+#[onelauncher_debug::debugger]
+pub async fn download_client(
+ st: &State,
+ version: &GameVersionInfo,
+ ingress: Option<&IngressId>,
+ force: bool,
+) -> crate::Result<()> {
+ let version_id = &version.id;
+ tracing::debug!("loading minecraft client for minecraft version {version_id}");
+
+ let client = version
+ .downloads
+ .get(&ip::api::minecraft::DownloadType::Client)
+ .ok_or(anyhow::anyhow!(
+ "no client downloads exist for {version_id}"
+ ))?;
+
+ let path = st
+ .directories
+ .version_dir(version_id)
+ .await
+ .join(format!("{version_id}.jar"));
+
+ if !path.exists() || force {
+ let result = fetch(&client.url, Some(&client.sha1), &st.fetch_semaphore).await?;
+
+ write(&path, &result, &st.io_semaphore).await?;
+ tracing::trace!("fetched minecraft client version {version_id}");
+ }
+
+ if let Some(ingress) = ingress {
+ send_ingress(ingress, 9.0, None).await?;
+ }
+
+ tracing::debug!("downloaded client for minecraft version {version_id}");
+ Ok(())
+}
diff --git a/core/src/game/mod.rs b/core/src/game/mod.rs
new file mode 100644
index 00000000..b908d868
--- /dev/null
+++ b/core/src/game/mod.rs
@@ -0,0 +1,552 @@
+//! **OneLauncher Game**
+//!
+//! Manages, installs, and launches the core Minecraft game.
+
+use crate::prelude::JavaVersion;
+use crate::processor;
+use crate::proxy::send::{init_or_edit_ingress, send_ingress};
+use crate::proxy::{IngressId, IngressType};
+use crate::store::{
+ self as st, Cluster, ClusterStage, MinecraftCredentials, ProcessorChild, State,
+};
+use crate::utils::io::{self, IOError};
+
+use chrono::Utc;
+use interpulse as ip;
+use interpulse::api::minecraft::{RuleAction, VersionInfo};
+use std::collections::HashMap;
+use std::sync::Arc;
+use tokio::process::Command;
+use uuid::Uuid;
+
+mod arguments;
+pub mod metadata;
+
+macro_rules! data_entry {
+ ($dest:expr; $($name:literal: client => $client:expr, server => $server:expr;)+) => {
+ $(std::collections::HashMap::insert(
+ $dest,
+ String::from($name),
+ interpulse::api::modded::SidedDataEntry {
+ client: String::from($client),
+ server: String::from($server),
+ },
+ );)+
+ }
+}
+
+#[tracing::instrument(skip(cluster))]
+#[onelauncher_debug::debugger]
+pub async fn install_minecraft(
+ cluster: &Cluster,
+ old_ingress: Option,
+ repair: bool,
+) -> crate::Result<()> {
+ let should_sync = old_ingress.is_some();
+ let ingress = init_or_edit_ingress(
+ old_ingress,
+ IngressType::DownloadLoader {
+ cluster_name: cluster.meta.name.clone(),
+ cluster_path: cluster.get_full_path().await?,
+ },
+ 100.0,
+ "downloading minecraft loader",
+ )
+ .await?;
+
+ crate::api::cluster::edit(&cluster.cluster_path(), |clus| {
+ clus.stage = ClusterStage::Downloading;
+
+ async { Ok(()) }
+ })
+ .await?;
+
+ State::sync().await?;
+
+ if should_sync {
+ Cluster::sync_packages(cluster.cluster_path(), true);
+ }
+
+ let state = State::get().await?;
+ let instance_path = &io::canonicalize(cluster.get_full_path().await?)?;
+ let metadata = state.metadata.read().await;
+
+ let version_idx = metadata
+ .minecraft
+ .versions
+ .iter()
+ .position(|g| g.id == cluster.meta.mc_version)
+ .ok_or(anyhow::anyhow!(
+ "invalid minecraft game version {}",
+ cluster.meta.mc_version
+ ))?;
+
+ let version = &metadata.minecraft.versions[version_idx];
+ let updated = version_idx
+ <= metadata
+ .minecraft
+ .versions
+ .iter()
+ .position(|g| g.id == "22w16a")
+ .unwrap_or(0); // LWJGL patching
+ let version_jar = cluster
+ .meta
+ .loader_version
+ .as_ref()
+ .map_or(version.id.clone(), |g| {
+ format!("{}-{}", version.id.clone(), g.id.clone())
+ });
+ let mut version_info = metadata::download_version_info(
+ &state,
+ version,
+ cluster.meta.loader_version.as_ref(),
+ Some(repair),
+ Some(&ingress),
+ )
+ .await?;
+ // TODO: java checking and installation
+ let key = version_info
+ .java_version
+ .as_ref()
+ .map(|it| it.major_version)
+ .unwrap_or(8);
+ let (java_version, set_java) =
+ if let Some(java_version) = java_version_from_cluster(cluster, &version_info).await? {
+ (std::path::PathBuf::from(java_version.path), false)
+ } else {
+ (crate::api::java::install_java(key).await?, true)
+ };
+
+ let java_version = crate::api::java::check_java(java_version.clone())
+ .await?
+ .ok_or_else(|| anyhow::anyhow!("java path validation failed: {:?}", java_version))?;
+
+ if set_java {
+ {
+ let mut settings = state.settings.write().await;
+ settings
+ .java_versions
+ .insert(format!("JAVA_{key}"), java_version.clone());
+ }
+ State::sync().await?;
+ }
+
+ metadata::download_minecraft(
+ &state,
+ &version_info,
+ &ingress,
+ &java_version.arch,
+ repair,
+ updated,
+ )
+ .await?;
+
+ if let Some(processors) = &version_info.processors {
+ let client = state
+ .directories
+ .version_dir(&version_jar)
+ .await
+ .join(format!("{version_jar}.jar"));
+ let libraries = state.directories.libraries_dir().await;
+ if let Some(ref mut data) = version_info.data {
+ data_entry! {
+ data;
+ "SIDE":
+ client => "client",
+ server => "";
+ "MINECRAFT_JAR":
+ client => client.to_string_lossy(),
+ server => "";
+ "MINECRAFT_VERSION":
+ client => cluster.meta.mc_version.clone(),
+ server => "";
+ "ROOT":
+ client => instance_path.to_string_lossy(),
+ server => "";
+ "LIBRARY_DIR":
+ client => libraries.to_string_lossy(),
+ server => "";
+ }
+
+ send_ingress(&ingress, 0.0, Some("running forge processors")).await?;
+ let total_length = processors.len();
+ for (index, processor) in processors.iter().enumerate() {
+ if let Some(sides) = &processor.sides {
+ if !sides.contains(&String::from("client")) {
+ continue;
+ }
+ }
+
+ let cp = ref_owned!(cp = processor.classpath.clone() => {
+ cp.push(processor.jar.clone())
+ });
+
+ let child = Command::new(&java_version.path)
+ .arg("-cp")
+ .arg(arguments::get_classpath_library(
+ &libraries,
+ &cp,
+ &java_version.arch,
+ )?)
+ .arg(
+ arguments::main_class(arguments::get_library(
+ &libraries,
+ &processor.jar,
+ false,
+ )?)
+ .await?
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "failed to find processor main class for {}",
+ processor.jar
+ )
+ })?,
+ )
+ .args(arguments::processor_arguments(
+ &libraries,
+ &processor.args,
+ data,
+ )?)
+ .output()
+ .await
+ .map_err(|e| IOError::with_path(e, &java_version.path))
+ .map_err(|err| anyhow::anyhow!("failed to run processor: {err}"))?;
+
+ if !child.status.success() {
+ return Err(anyhow::anyhow!(
+ "error occured while running processor: {}",
+ String::from_utf8_lossy(&child.stderr)
+ )
+ .into());
+ }
+
+ send_ingress(
+ &ingress,
+ 30.0 / total_length as f64,
+ Some(&format!(
+ "running forge processor {}/{}",
+ index, total_length
+ )),
+ )
+ .await?;
+ }
+ }
+ }
+
+ crate::api::cluster::edit(&cluster.cluster_path(), |clus| {
+ clus.stage = ClusterStage::Installed;
+
+ async { Ok(()) }
+ })
+ .await?;
+ State::sync().await?;
+ send_ingress(&ingress, 1.0, Some("installed minecraft successfully")).await?;
+
+ Ok(())
+}
+
+#[tracing::instrument(skip_all)]
+#[onelauncher_debug::debugger]
+#[allow(clippy::too_many_arguments)]
+pub async fn launch_minecraft(
+ cluster: &Cluster,
+ java_args: &[String],
+ env_args: &[(String, String)],
+ mc_options: &[(String, String)],
+ post_hook: Option,
+ credentials: &MinecraftCredentials,
+ resolution: &st::Resolution,
+ memory: &st::Memory,
+ wrapper: &Option,
+) -> crate::Result>> {
+ if cluster.stage == ClusterStage::PackDownloading || cluster.stage == ClusterStage::Downloading
+ {
+ return Err(anyhow::anyhow!("cluster is still downloading").into());
+ }
+
+ if cluster.stage != ClusterStage::Installed {
+ install_minecraft(cluster, None, false).await?;
+ }
+
+ let state = State::get().await?;
+ let metadata = state.metadata.read().await;
+ let instance_path = cluster.get_full_path().await?;
+ let instance_path = &io::canonicalize(instance_path)?;
+
+ let version_index = metadata
+ .minecraft
+ .versions
+ .iter()
+ .position(|it| it.id == cluster.meta.mc_version)
+ .ok_or(anyhow::anyhow!(
+ "invalid game version {}",
+ cluster.meta.mc_version
+ ))?;
+ let version = &metadata.minecraft.versions[version_index];
+ let updated = version_index
+ <= metadata
+ .minecraft
+ .versions
+ .iter()
+ .position(|x| x.id == "22w16a")
+ .unwrap_or(0);
+ let version_jar = cluster
+ .meta
+ .loader_version
+ .as_ref()
+ .map_or(version.id.clone(), |it| {
+ format!("{}-{}", version.id.clone(), it.id.clone())
+ });
+ let version_info = metadata::download_version_info(
+ &state,
+ version,
+ cluster.meta.loader_version.as_ref(),
+ None,
+ None,
+ )
+ .await?;
+ let java_version = java_version_from_cluster(cluster, &version_info)
+ .await?
+ .ok_or_else(|| anyhow::anyhow!("missing java installation"))?;
+ let java_version = crate::api::java::check_java(java_version.path.clone().into())
+ .await?
+ .ok_or_else(|| anyhow::anyhow!("java path invalid: {}", java_version.path))?;
+ let client_path = state
+ .directories
+ .version_dir(&version_jar)
+ .await
+ .join(format!("{version_jar}.jar"));
+ let args = version_info.arguments.clone().unwrap_or_default();
+ let mut command = match wrapper {
+ Some(hook) => ref_owned!(it = Command::new(hook) => {it.arg(&java_version.path)}),
+ None => Command::new(&java_version.path),
+ };
+ let env_args = Vec::from(env_args);
+ let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?;
+ if let Some(uuid) = existing.first() {
+ return Err(anyhow::anyhow!(
+ "cluster {} is already running ({uuid})",
+ cluster.cluster_path()
+ )
+ .into());
+ }
+
+ command
+ .args(
+ arguments::java_arguments(
+ args.get(&ip::api::minecraft::ArgumentType::Jvm)
+ .map(|x| x.as_slice()),
+ &state.directories.version_natives_dir(&version_jar).await,
+ &state.directories.libraries_dir().await,
+ &arguments::classpaths(
+ &state.directories.libraries_dir().await,
+ version_info.libraries.as_slice(),
+ &client_path,
+ &java_version.arch,
+ updated,
+ )?,
+ &version_jar,
+ *memory,
+ Vec::from(java_args),
+ &java_version.arch,
+ )?
+ .into_iter()
+ .collect::>(),
+ )
+ .arg(version_info.main_class.clone())
+ .args(
+ arguments::minecraft_arguments(
+ args.get(&ip::api::minecraft::ArgumentType::Game)
+ .map(|a| a.as_slice()),
+ version_info.minecraft_arguments.as_deref(),
+ credentials,
+ &version.id,
+ &version_info.asset_index.id,
+ instance_path,
+ &state.directories.assets_dir().await,
+ &version.type_,
+ *resolution,
+ &java_version.arch,
+ )?
+ .into_iter()
+ .collect::>(),
+ )
+ .current_dir(instance_path.clone());
+
+ // when cargo makes the DYLD_LIBRARY_PATH it breaks Minecraft
+ #[cfg(target_os = "macos")]
+ if std::env::var("CARGO").is_ok() {
+ command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
+ }
+
+ // remove preexisting Java options, as they should be set in the cluster settings.
+ command.env_remove("_JAVA_OPTIONS");
+ command.envs(env_args);
+
+ // overrides `options.txt` with our settings: i can't believe it's not yaml
+ use regex::Regex;
+
+ if !mc_options.is_empty() {
+ let options_path = instance_path.join("options.txt");
+ let mut options_string = String::new();
+ if options_path.exists() {
+ options_string = io::read_to_string(&options_path).await?;
+ }
+ for (key, value) in mc_options {
+ let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
+ if !re.is_match(&options_string) {
+ options_string.push_str(&format!("\n{}:{}", key, value));
+ } else {
+ let replaced_string = re
+ .replace_all(&options_string, &format!("{}:{}", key, value))
+ .to_string();
+ options_string = replaced_string;
+ }
+ }
+
+ io::write(&options_path, options_string).await?;
+ }
+
+ crate::api::cluster::edit(&cluster.cluster_path(), |clust| {
+ clust.meta.played_at = Some(Utc::now());
+
+ async { Ok(()) }
+ })
+ .await?;
+ State::sync().await?;
+
+ let mut censors = HashMap::new();
+ let username = whoami::username();
+ let realname = whoami::realname();
+ censors.insert(format!("/{}/", username), "/{ENV_USERNAME}/".to_string());
+ censors.insert(
+ format!("\\{}\\", username),
+ "\\{ENV_USERNAME}\\".to_string(),
+ );
+ censors.insert(format!("/{}/", realname), "/{ENV_REALNAME}/".to_string());
+ censors.insert(
+ format!("\\{}\\", realname),
+ "\\{ENV_REALNAME}\\".to_string(),
+ );
+ censors.insert(
+ credentials.access_token.clone(),
+ "{MC_ACCESS_TOKEN}".to_string(),
+ );
+ censors.insert(credentials.username.clone(), "{MC_USERNAME}".to_string());
+ censors.insert(
+ credentials.id.as_simple().to_string(),
+ "{MC_UUID}".to_string(),
+ );
+ censors.insert(
+ credentials.id.as_hyphenated().to_string(),
+ "{MC_UUID}".to_string(),
+ );
+
+ #[cfg(feature = "tauri")]
+ {
+ use crate::ProxyState;
+ let window = ProxyState::get_main_window().await?;
+ let settings = state.settings.read().await;
+ if settings.hide_on_launch {
+ window.minimize()?;
+ }
+ }
+
+ if !*state.offline.read().await {
+ let _ = state
+ .discord_rpc
+ .set_activity(&format!("Playing {}", cluster.meta.name), true)
+ .await;
+ }
+
+ let mut state_processor = state.processor.write().await;
+ state_processor
+ .insert_process(
+ Uuid::new_v4(),
+ cluster.cluster_path(),
+ command,
+ post_hook,
+ censors,
+ )
+ .await
+}
+
+#[tracing::instrument]
+pub fn rules(rules: &[ip::api::minecraft::Rule], java_version: &str, updated: bool) -> bool {
+ let mut rule = rules
+ .iter()
+ .map(|r| rule(r, java_version, updated))
+ .collect::>>();
+ if rules
+ .iter()
+ .all(|r| matches!(r.action, RuleAction::Disallow))
+ {
+ rule.push(Some(true))
+ }
+
+ !(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(|r| r.is_none()))
+}
+
+#[tracing::instrument]
+pub fn rule(rule: &ip::api::minecraft::Rule, java_version: &str, updated: bool) -> Option {
+ use ip::api::minecraft::{Rule, RuleAction};
+
+ let result = match rule {
+ Rule {
+ os: Some(ref os), ..
+ } => crate::utils::platform::os_rule(os, java_version, updated),
+ Rule {
+ features: Some(ref features),
+ ..
+ } => {
+ !features.is_demo_user.unwrap_or(true)
+ || features.has_custom_resolution.unwrap_or(false)
+ || !features.has_quick_plays_support.unwrap_or(true)
+ || !features.is_quick_play_multiplayer.unwrap_or(true)
+ || !features.is_quick_play_realms.unwrap_or(true)
+ || !features.is_quick_play_singleplayer.unwrap_or(true)
+ }
+ _ => return Some(true),
+ };
+
+ match rule.action {
+ RuleAction::Allow => {
+ if result {
+ Some(true)
+ } else {
+ None
+ }
+ }
+ RuleAction::Disallow => {
+ if result {
+ Some(false)
+ } else {
+ None
+ }
+ }
+ }
+}
+
+pub async fn java_version_from_cluster(
+ cluster: &Cluster,
+ version_info: &VersionInfo,
+) -> crate::Result> {
+ if let Some(java) = cluster.java.clone().and_then(|x| x.custom_version) {
+ Ok(Some(java))
+ } else {
+ let key = version_info
+ .java_version
+ .as_ref()
+ .map(|x| x.major_version)
+ .unwrap_or(8);
+
+ let state = State::get().await?;
+ let settings = state.settings.read().await;
+
+ if let Some(j) = settings.java_versions.get(&format!("JAVA_{key}")) {
+ return Ok(Some(j.clone()));
+ }
+
+ Ok(None)
+ }
+}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 02b406a9..25014d6a 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -1,5 +1,5 @@
//! # OneLauncher
-//!
+//!
//! A library used as a core for our launcher and Rust APIs.
#![warn(unused_import_braces, missing_debug_implementations)]
@@ -15,9 +15,8 @@ pub mod game;
pub mod logger;
pub mod store;
+pub use api::proxy::{Ingress, IngressType, ProxyState};
pub use api::*;
pub use error::*;
-pub use api::proxy::{ProxyState, Ingress, IngressType};
pub use logger::start_logger;
-pub use store::InnerPathLinux;
-pub use store::State;
+pub use store::{InnerPathLinux, State};
diff --git a/core/src/logger.rs b/core/src/logger.rs
index c4162b5b..1d742da0 100644
--- a/core/src/logger.rs
+++ b/core/src/logger.rs
@@ -1,16 +1,16 @@
//! **OneLauncher Logger**
-//!
+//!
//! Public utilities for [`tracing`] logging in OneLauncher.
-//!
+//!
//! - `RUST_LOG=xxx` is a variable which defines tracing configuration (`ERROR > WARN > INFO > DEBUG > TRACE`)
//! - `RUST_LOG="onelauncher=trace"` will show **all** mesages from OneLauncher and not other dependencies.
//! - `RUST_LOG="info"` will show info, warn, and error logs.`
-//!
+//!
//! Errors returned to [`tauri`] are displayed as tracing error logs.
//! These errors also include an attached span trace if the error is from a [`tracing_error`] and the level is info, debug, or trace.
//! Tracing errors also provide Sentry metadata to be handled.
//! To set the environment variable on `unix` you can use the following command: `RUST_LOG="xxx" {your command}`
-//!
+//!
//! The default `RUST_LOG` is `onelauncher=show`, meaning only logs from OneLauncher will be displayed at the info, warn, and error levels.
use tracing_appender::non_blocking::WorkerGuard;
diff --git a/core/src/store/clusters.rs b/core/src/store/clusters.rs
index 5dd72a1f..1a47a508 100644
--- a/core/src/store/clusters.rs
+++ b/core/src/store/clusters.rs
@@ -1,17 +1,19 @@
//! Core cluster state management
-use std::{collections::HashMap, path::{Path, PathBuf}};
use chrono::{DateTime, Utc};
use futures::prelude::*;
use interpulse::api::modded::LoaderVersion;
use notify::RecommendedWatcher;
use notify_debouncer_mini::Debouncer;
-use serde::{Serialize, Deserialize};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
use uuid::Uuid;
-use crate::proxy::{send::{send_cluster, send_message}, ClusterPayloadType};
-use crate::utils::{http::IoSemaphore, io::{self, IOError}};
-use crate::utils::http::write_icon;
+use crate::proxy::send::{send_cluster, send_message};
+use crate::proxy::ClusterPayloadType;
+use crate::utils::http::{write_icon, IoSemaphore};
+use crate::utils::io::{self, IOError};
use crate::utils::java::JavaVersion;
use crate::State;
@@ -27,15 +29,15 @@ pub(crate) struct Clusters(pub HashMap);
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ClusterStage {
- /// Installed with no downloads left.
- Installed,
- /// Downloading any sort of core metadata or mod file.
- Downloading,
- /// Downloading a full pack file, which is granted lower priority due to the high network demand.
- PackDownloading,
- /// Not installed at all.
- #[default]
- NotInstalled,
+ /// Installed with no downloads left.
+ Installed,
+ /// Downloading any sort of core metadata or mod file.
+ Downloading,
+ /// Downloading a full pack file, which is granted lower priority due to the high network demand.
+ PackDownloading,
+ /// Not installed at all.
+ #[default]
+ NotInstalled,
}
/// Relative Path wrapper to be used as an identifer for a cluster path.
@@ -44,38 +46,46 @@ pub enum ClusterStage {
pub struct ClusterPath(pub PathBuf);
impl ClusterPath {
- /// Get a new [`ClusterPath`] instance from an [`Into`].
- pub fn new(path: impl Into) -> Self {
- ClusterPath(path.into())
- }
+ /// Get a new [`ClusterPath`] instance from an [`Into`].
+ pub fn new(path: impl Into) -> Self {
+ ClusterPath(path.into())
+ }
- /// Get the full [`PathBuf`] of the current cluster path.
- pub async fn full_path(&self) -> crate::Result {
- let state = State::get().await?;
- let clusters_dir = state.directories.clusters_dir().await;
- Ok(clusters_dir.join(&self.0))
- }
+ /// Get the full [`PathBuf`] of the current cluster path.
+ pub async fn full_path(&self) -> crate::Result {
+ let state = State::get().await?;
+ let clusters_dir = state.directories.clusters_dir().await;
+ Ok(clusters_dir.join(&self.0))
+ }
- /// Validate the UTF of a cluster path.
- pub fn validate(&self) -> crate::Result<&Self> {
- self.0.to_str().ok_or(anyhow::anyhow!("invalid file path string {}!", self.0.clone().to_string_lossy()));
- Ok(self)
- }
+ /// Validate the UTF of a cluster path.
+ pub fn validate(&self) -> crate::Result<&Self> {
+ self.0.to_str().ok_or(anyhow::anyhow!(
+ "invalid file path string {}!",
+ self.0.clone().to_string_lossy()
+ ));
+ Ok(self)
+ }
- /// Create a [`ClusterPath`] from a full [`PathBuf`].
- pub async fn from_path(path: PathBuf) -> crate::Result {
- let path: PathBuf = io::canonicalize(path)?;
- let clusters_dir = io::canonicalize(State::get().await?.directories.clusters_dir().await)?;
- path.strip_prefix(clusters_dir).ok().and_then(|f| f.file_name()).ok_or_else(|| anyhow::anyhow!("path {} is not a cluster path", path.to_string_lossy()))?;
+ /// Create a [`ClusterPath`] from a full [`PathBuf`].
+ pub async fn from_path(path: PathBuf) -> crate::Result {
+ let path: PathBuf = io::canonicalize(path)?;
+ let clusters_dir = io::canonicalize(State::get().await?.directories.clusters_dir().await)?;
+ path.strip_prefix(clusters_dir)
+ .ok()
+ .and_then(|f| f.file_name())
+ .ok_or_else(|| {
+ anyhow::anyhow!("path {} is not a cluster path", path.to_string_lossy())
+ })?;
- Ok(Self(path))
- }
+ Ok(Self(path))
+ }
}
impl std::fmt::Display for ClusterPath {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.display().fmt(f)
- }
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.display().fmt(f)
+ }
}
/// Used for backwards compatibility for modpacks which handle windows paths strangely.
@@ -84,26 +94,26 @@ impl std::fmt::Display for ClusterPath {
pub struct InnerPathLinux(pub String);
impl InnerPathLinux {
- /// Get the first 2 components of the inner path.
- pub fn get_components(&self) -> String {
- self.to_string()
- .split('/')
- .take(2)
- .collect::>()
- .join("/")
- }
+ /// Get the first 2 components of the inner path.
+ pub fn get_components(&self) -> String {
+ self.to_string()
+ .split('/')
+ .take(2)
+ .collect::>()
+ .join("/")
+ }
}
impl std::fmt::Display for InnerPathLinux {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.0)
- }
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
}
impl From for InnerPathLinux {
- fn from(value: RawPackagePath) -> Self {
- InnerPathLinux(value.0.replace('\\', "/"))
- }
+ fn from(value: RawPackagePath) -> Self {
+ InnerPathLinux(value.0.replace('\\', "/"))
+ }
}
/// Used for backwards compatibility for modpacks which handle windows paths strangely.
@@ -112,9 +122,9 @@ impl From for InnerPathLinux {
struct RawPackagePath(pub String);
impl From for RawPackagePath {
- fn from(value: InnerPathLinux) -> Self {
- RawPackagePath(value.0)
- }
+ fn from(value: InnerPathLinux) -> Self {
+ RawPackagePath(value.0)
+ }
}
/// Relative [`PathBuf`] for a specific [`Package`] of a [`Cluster`].
@@ -123,575 +133,630 @@ impl From for RawPackagePath {
pub struct PackagePath(pub PathBuf);
impl PackagePath {
- /// Transform a full [`PathBuf`] into a relative [`PackagePath`].
- pub async fn from_path(path: &PathBuf) -> crate::Result {
- let clusters_dir: PathBuf = std::fs::canonicalize(
- State::get().await?.directories.clusters_dir().await
- )?;
- let path: PathBuf = std::fs::canonicalize(path)?;
- let path = path
- .strip_prefix(clusters_dir)
- .ok()
- .map(|p| p.components().skip(1).collect::())
- .ok_or_else(|| anyhow::anyhow!("path {path:?} does not exist in a cluster!", path = path))?;
+ /// Transform a full [`PathBuf`] into a relative [`PackagePath`].
+ pub async fn from_path(path: &PathBuf) -> crate::Result {
+ let clusters_dir: PathBuf =
+ std::fs::canonicalize(State::get().await?.directories.clusters_dir().await)?;
+ let path: PathBuf = std::fs::canonicalize(path)?;
+ let path = path
+ .strip_prefix(clusters_dir)
+ .ok()
+ .map(|p| p.components().skip(1).collect::())
+ .ok_or_else(|| {
+ anyhow::anyhow!("path {path:?} does not exist in a cluster!", path = path)
+ })?;
- Ok(Self(path))
- }
+ Ok(Self(path))
+ }
- /// Get the full [`PathBuf`] of the current package path.
- pub async fn full_path(&self, cluster: ClusterPath) -> crate::Result {
- let cluster_dir = cluster.full_path().await?;
- Ok(cluster_dir.join(&self.0))
- }
+ /// Get the full [`PathBuf`] of the current package path.
+ pub async fn full_path(&self, cluster: ClusterPath) -> crate::Result {
+ let cluster_dir = cluster.full_path().await?;
+ Ok(cluster_dir.join(&self.0))
+ }
- /// Get the [`InnerPathLinux`] of a [`PackagePath`].
- pub fn inner_path(&self) -> InnerPathLinux {
- InnerPathLinux(self.0.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::>().join("/"))
- }
+ /// Get the [`InnerPathLinux`] of a [`PackagePath`].
+ pub fn inner_path(&self) -> InnerPathLinux {
+ InnerPathLinux(
+ self.0
+ .components()
+ .map(|c| c.as_os_str().to_string_lossy().to_string())
+ .collect::>()
+ .join("/"),
+ )
+ }
- /// Create a new PackagePath from a relative path.
- pub fn new(path: &Path) -> Self {
- PackagePath(PathBuf::from(path))
- }
+ /// Create a new PackagePath from a relative path.
+ pub fn new(path: &Path) -> Self {
+ PackagePath(PathBuf::from(path))
+ }
}
/// Represents a single Instance and installation of Minecraft
/// Contains settings and identifiers on a per-Cluster basis, falling back to default settings for Options<>
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Cluster {
- /// The Uuid of a specific cluster.
- pub uuid: Uuid,
- /// The download stage that of a cluster.
- #[serde(default)]
- pub stage: ClusterStage,
- /// The core path of the cluster.
- #[serde(default)]
- pub path: PathBuf,
- /// The associated cluster metadata.
- pub meta: ClusterMeta,
- /// The per-cluster JVM memory allocation options.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub memory: Option,
- /// The per-cluster JVM options.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub java: Option,
- /// The per-cluster Minecraft window default resolution.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub resolution: Option,
- /// The per-cluster Minecraft window fullscreen status.
- pub force_fullscreen: Option,
- // The per-cluster initialization hooks.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub init_hooks: Option,
- pub packages: HashMap,
- #[serde(default)]
- pub update: Option,
+ /// The Uuid of a specific cluster.
+ pub uuid: Uuid,
+ /// The download stage that of a cluster.
+ #[serde(default)]
+ pub stage: ClusterStage,
+ /// The core path of the cluster.
+ #[serde(default)]
+ pub path: PathBuf,
+ /// The associated cluster metadata.
+ pub meta: ClusterMeta,
+ /// The per-cluster JVM memory allocation options.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub memory: Option,
+ /// The per-cluster JVM options.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub java: Option,
+ /// The per-cluster Minecraft window default resolution.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resolution: Option,
+ /// The per-cluster Minecraft window fullscreen status.
+ pub force_fullscreen: Option,
+ // The per-cluster initialization hooks.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub init_hooks: Option,
+ pub packages: HashMap,
+ #[serde(default)]
+ pub update: Option,
}
/// Represents core Cluster metadata ([`Cluster#meta`]).
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ClusterMeta {
- /// The user-facing name of the cluster stored as a string.
- pub name: String,
+ /// The user-facing name of the cluster stored as a string.
+ pub name: String,
- /// The user-facing group of the cluster stored as a string.
- #[serde(default)]
- pub group: Vec,
- /// The associated Minecraft version of the cluster as last updated.
- pub mc_version: String,
+ /// The user-facing group of the cluster stored as a string.
+ #[serde(default)]
+ pub group: Vec,
+ /// The associated Minecraft version of the cluster as last updated.
+ pub mc_version: String,
- /// The associated mod [`Loader`] as specified in the cluster.
- #[serde(default)]
- pub loader: Loader,
- /// The associated mod [`LoaderVersion`] if available.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub loader_version: Option,
+ /// The associated mod [`Loader`] as specified in the cluster.
+ #[serde(default)]
+ pub loader: Loader,
+ /// The associated mod [`LoaderVersion`] if available.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub loader_version: Option,
- /// The time that the cluster was created in [`DateTime`].
- #[serde(default)]
- pub created_at: DateTime,
- /// The time the cluster was last modified in [`DateTime`].
- #[serde(default)]
- pub modified_at: DateTime,
- /// The last time the cluster was played in [`DateTime`].
- /// (Defaults to None if the cluster has never been played)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub played_at: Option>,
- /// The overall time played stored as a [`u64`].
- #[serde(default)]
- pub overall_played: u64,
- /// The recent time played stored as a [`u64`].
- #[serde(default)]
- pub recently_played: u64,
+ /// The time that the cluster was created in [`DateTime`].
+ #[serde(default)]
+ pub created_at: DateTime,
+ /// The time the cluster was last modified in [`DateTime`].
+ #[serde(default)]
+ pub modified_at: DateTime,
+ /// The last time the cluster was played in [`DateTime`].
+ /// (Defaults to None if the cluster has never been played)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub played_at: Option>,
+ /// The overall time played stored as a [`u64`].
+ #[serde(default)]
+ pub overall_played: u64,
+ /// The recent time played stored as a [`u64`].
+ #[serde(default)]
+ pub recently_played: u64,
- /// The associated [`PackageData`] and modpack data for the cluster.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub package_data: Option,
+ /// The associated [`PackageData`] and modpack data for the cluster.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub package_data: Option,
- /// The user-facing cluster icon as a [`PathBuf`].
- #[serde(skip_serializing_if = "Option::is_none")]
- pub icon: Option,
- /// The user-facing cluster icon as a URL string.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub icon_url: Option
+ /// The user-facing cluster icon as a [`PathBuf`].
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon: Option,
+ /// The user-facing cluster icon as a URL string.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon_url: Option,
}
/// Optional data used to link a specific cluster to a package project.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackageData {
- /// The package ID as a String.
- pub package_id: Option,
- /// The version of the package as a String.
- pub version_id: Option,
- /// Whether or not the current package is locked (for legacy modpack support).
- #[serde(default = "default_locked")]
- pub locked: Option,
+ /// The package ID as a String.
+ pub package_id: Option,
+ /// The version of the package as a String.
+ pub version_id: Option,
+ /// Whether or not the current package is locked (for legacy modpack support).
+ #[serde(default = "default_locked")]
+ pub locked: Option,
}
-pub fn default_locked() -> Option { Some(true) }
+pub fn default_locked() -> Option {
+ Some(true)
+}
/// Available mod loaders to be used for a cluster.
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Loader {
- /// The default vanilla loader, no modding supported.
- #[default]
- Vanilla,
- /// The MinecraftForge Minecraft mod loader.
- Forge,
- /// The FabircMC Minecraft mod loader.
- Fabric,
- /// The NeoForge Minecraft mod loader.
- NeoForge,
- /// The Quilt Minecraft mod loader.
- Quilt,
- /// The Legacy Fabric port mod loader.
- LegacyFabric
+ /// The default vanilla loader, no modding supported.
+ #[default]
+ Vanilla,
+ /// The MinecraftForge Minecraft mod loader.
+ Forge,
+ /// The FabircMC Minecraft mod loader.
+ Fabric,
+ /// The NeoForge Minecraft mod loader.
+ NeoForge,
+ /// The Quilt Minecraft mod loader.
+ Quilt,
+ /// The Legacy Fabric port mod loader.
+ LegacyFabric,
}
impl std::fmt::Display for Loader {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(match *self {
- Self::Vanilla => "Vanilla",
- Self::Forge => "Forge",
- Self::Fabric => "Fabric",
- Self::NeoForge => "NeoForge",
- Self::Quilt => "Quilt",
- Self::LegacyFabric => "LegacyFabric",
- })
- }
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match *self {
+ Self::Vanilla => "Vanilla",
+ Self::Forge => "Forge",
+ Self::Fabric => "Fabric",
+ Self::NeoForge => "NeoForge",
+ Self::Quilt => "Quilt",
+ Self::LegacyFabric => "LegacyFabric",
+ })
+ }
}
impl Loader {
- /// Get the loader version to lowercase for metadata fetching.
- pub(crate) fn as_meta(&self) -> &'static str {
- match *self {
- Self::Vanilla => "vanilla",
- Self::Forge => "forge",
- Self::Fabric => "fabric",
- Self::NeoForge => "neoforge",
- Self::Quilt => "quilt",
- Self::LegacyFabric => "legacyfabric",
- }
- }
+ /// Get the loader version to lowercase for metadata fetching.
+ pub(crate) fn as_meta(&self) -> &'static str {
+ match *self {
+ Self::Vanilla => "vanilla",
+ Self::Forge => "forge",
+ Self::Fabric => "fabric",
+ Self::NeoForge => "neoforge",
+ Self::Quilt => "quilt",
+ Self::LegacyFabric => "legacyfabric",
+ }
+ }
}
/// Custom Java arguments on a per-cluster basis.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct JavaOptions {
- /// A custom java version from the global java store, if specified.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub custom_version: Option,
- /// Custom runtime arguments when running the cluster.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub custom_arguments: Option>,
- /// Custom environment variables when running the cluster.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub custom_env_arguments: Option