You've already forked AstralRinth
forked from didirus/AstralRinth
Refactor Library
The launcher code was in a position ripe for sphagetti, so this rewrites it in a more robust way. In addition to cleaner code, this provides the following changes: - Removal of obsolete Mojang authentication - The rebasing of some internal state into a Sled database - Tweaks which make some internal mechanisms more robust (e.g. profiles which fail to load can be removed) - Additional tooling integration such as direnv - Distinct public API to avoid messing with too much internal code - Unified error handling in the form of `theseus::Error` and `theseus::Result`
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,9 +3,10 @@ node_modules/
|
||||
.svelte-kit/
|
||||
theseus_gui/build/
|
||||
WixTools
|
||||
.direnv/
|
||||
|
||||
[#]*[#]
|
||||
|
||||
# TEMPORARY: ignore my test instance and metadata
|
||||
theseus_cli/launcher
|
||||
theseus_cli/foo
|
||||
|
||||
|
||||
2131
Cargo.lock
generated
2131
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
34
flake.lock
generated
34
flake.lock
generated
@@ -8,11 +8,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1646893503,
|
||||
"narHash": "sha256-N4Wn8FUXUC1h1DkL8X9I7VMvIv0fLLLjeJX3uFyzvRQ=",
|
||||
"lastModified": 1655706580,
|
||||
"narHash": "sha256-7DshIT1Ya5W9NAW7UdnYCHsGmXfOXJZCEHbbB/cCX7g=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "aad7f0a3e44ecfc9e2c5f1a45387d193c1c51aa6",
|
||||
"rev": "d895003d8e03ac2fc8ffe2aa898299cbef1a7048",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -28,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1639947939,
|
||||
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
|
||||
"lastModified": 1655042882,
|
||||
"narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
|
||||
"rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -43,11 +43,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1646497237,
|
||||
"narHash": "sha256-Ccpot1h/rV8MgcngDp5OrdmLTMaUTbStZTR5/sI7zW0=",
|
||||
"lastModified": 1655624069,
|
||||
"narHash": "sha256-7g1zwTdp35GMTERnSzZMWJ7PG3QdDE8VOX3WsnOkAtM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "062a0c5437b68f950b081bbfc8a699d57a4ee026",
|
||||
"rev": "0d68d7c857fe301d49cdcd56130e0beea4ecd5aa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -68,15 +68,15 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1646862342,
|
||||
"narHash": "sha256-zXd3qsIcQFDFMB6p8bSpkOKjTuBTvYuM4GkPYxEfQdA=",
|
||||
"owner": "rust-analyzer",
|
||||
"lastModified": 1655654433,
|
||||
"narHash": "sha256-auHQ0XPCiaTPSn+R3Yu4J7oZ5Zq/FS5/Da1ivvdYb/Y=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "5b51cb835a356cf79cba00cf5c65d51cadeea7f1",
|
||||
"rev": "427061da19723f2206fe4dcb175c9c43b9a6193d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-analyzer",
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
@@ -84,11 +84,11 @@
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1644229661,
|
||||
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
||||
"lastModified": 1653893745,
|
||||
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
||||
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
30
flake.nix
30
flake.nix
@@ -14,7 +14,7 @@
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs:
|
||||
outputs = inputs@{self, ...}:
|
||||
inputs.utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import inputs.nixpkgs { inherit system; };
|
||||
fenix = inputs.fenix.packages.${system};
|
||||
@@ -32,13 +32,15 @@
|
||||
|
||||
deps = with pkgs; {
|
||||
global = [
|
||||
openssl pkg-config
|
||||
openssl pkg-config gcc
|
||||
];
|
||||
gui = [
|
||||
gtk4 gdk-pixbuf atk webkitgtk
|
||||
gtk4 gdk-pixbuf atk webkitgtk dbus
|
||||
];
|
||||
shell = [
|
||||
toolchain fenix.default.clippy git
|
||||
toolchain
|
||||
(with fenix; combine [toolchain default.clippy rust-analyzer])
|
||||
git
|
||||
jdk17 jdk8
|
||||
];
|
||||
};
|
||||
@@ -53,8 +55,22 @@
|
||||
};
|
||||
|
||||
apps = {
|
||||
theseus-cli = utils.mkApp {
|
||||
drv = inputs.self.packages.${system}.theseus-cli;
|
||||
cli = utils.mkApp {
|
||||
drv = self.packages.${system}.theseus-cli;
|
||||
};
|
||||
cli-test = utils.mkApp {
|
||||
drv = pkgs.writeShellApplication {
|
||||
name = "theseus-test-cli";
|
||||
runtimeInputs = [
|
||||
(self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
|
||||
release = false;
|
||||
}))
|
||||
];
|
||||
text = ''
|
||||
DUMMY_ID="$(printf '%0.sa' {1..32})"
|
||||
theseus_cli profile run -t "" -n "Test" -i "$DUMMY_ID" "$@"
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -62,5 +78,5 @@
|
||||
buildInputs = with deps;
|
||||
global ++ gui ++ shell;
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ edition = "2018"
|
||||
thiserror = "1.0"
|
||||
async-trait = "0.1.51"
|
||||
|
||||
daedalus = "0.1.12"
|
||||
daedalus = { version = "0.1.16", features = ["bincode"] }
|
||||
|
||||
bincode = { version = "2.0.0-rc.1", features = ["serde"] }
|
||||
sled = { version = "0.34.7", features = ["compression"] }
|
||||
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -36,13 +39,9 @@ sys-info = "0.9.0"
|
||||
log = "0.4.14"
|
||||
const_format = "0.2.22"
|
||||
once_cell = "1.9.0"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dev-dependencies]
|
||||
argh = "0.1.6"
|
||||
pretty_assertions = "1.1.0"
|
||||
|
||||
[[example]]
|
||||
name = "download-pack"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
|
||||
use argh::FromArgs;
|
||||
use theseus::modpack::{fetch_modpack, pack::ModpackSide};
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// Simple modpack download
|
||||
struct ModpackDownloader {
|
||||
/// where to download to
|
||||
#[argh(positional)]
|
||||
url: String,
|
||||
|
||||
/// where to put the resulting pack
|
||||
#[argh(option, short = 'o')]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// the sha1 hash, if you want it checked
|
||||
#[argh(option, short = 'c')]
|
||||
hash: Option<String>,
|
||||
|
||||
/// use verbose logging
|
||||
#[argh(switch, short = 'v')]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// Simple logging helper
|
||||
fn debug(msg: &str, verbose: bool) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
let args = argh::from_env::<ModpackDownloader>();
|
||||
let dest = args.output.unwrap_or(PathBuf::from("./pack-download/"));
|
||||
|
||||
debug(
|
||||
&format!(
|
||||
"Downloading pack {} to {}",
|
||||
args.url,
|
||||
dest.to_str().unwrap_or("?")
|
||||
),
|
||||
args.verbose,
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
fetch_modpack(&args.url, args.hash.as_deref(), &dest, ModpackSide::Client)
|
||||
.await
|
||||
.unwrap();
|
||||
let end = start.elapsed();
|
||||
|
||||
println!("Download completed in {} seconds", end.as_secs_f32());
|
||||
debug("Done!", args.verbose);
|
||||
}
|
||||
20
theseus/src/api/mod.rs
Normal file
20
theseus/src/api/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! API for interacting with Theseus
|
||||
pub mod profile;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::{
|
||||
launcher::Credentials,
|
||||
state::{
|
||||
DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader,
|
||||
ProfileMetadata, Settings, WindowSize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
data::*,
|
||||
profile::{self, Profile},
|
||||
State,
|
||||
};
|
||||
}
|
||||
215
theseus/src/api/profile.rs
Normal file
215
theseus/src/api/profile.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
pub use crate::{
|
||||
state::{JavaSettings, Profile},
|
||||
State,
|
||||
};
|
||||
use daedalus as d;
|
||||
use std::{future::Future, path::Path};
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
/// Add a profile to the in-memory state
|
||||
pub async fn add(profile: Profile) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
profiles.insert(profile)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a path as a profile in-memory
|
||||
pub async fn add_path(path: &Path) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
profiles.insert_from(path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a profile
|
||||
pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
profiles.remove(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a profile by path,
|
||||
pub async fn get(path: &Path) -> crate::Result<Option<Profile>> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
|
||||
profiles.0.get(path).map_or(Ok(None), |prof| match prof {
|
||||
Some(prof) => Ok(Some(prof.clone())),
|
||||
None => Err(crate::Error::UnloadedProfileError(
|
||||
path.display().to_string(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a profile is already managed by Theseus
|
||||
pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
Ok(profiles.0.contains_key(profile))
|
||||
}
|
||||
|
||||
/// Check if a profile is loaded
|
||||
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
Ok(profiles
|
||||
.0
|
||||
.get(profile)
|
||||
.map(Option::as_ref)
|
||||
.flatten()
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Edit a profile using a given asynchronous closure
|
||||
pub async fn edit<Fut>(
|
||||
path: &Path,
|
||||
action: impl Fn(&mut Profile) -> Fut,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
Fut: Future<Output = crate::Result<()>>,
|
||||
{
|
||||
let state = State::get().await.unwrap();
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
match profiles.0.get_mut(path) {
|
||||
Some(&mut Some(ref mut profile)) => action(profile).await,
|
||||
Some(&mut None) => Err(crate::Error::UnloadedProfileError(
|
||||
path.display().to_string(),
|
||||
)),
|
||||
None => Err(crate::Error::UnmanagedProfileError(
|
||||
path.display().to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run Minecraft using a profile
|
||||
pub async fn run(
|
||||
path: &Path,
|
||||
credentials: &crate::launcher::Credentials,
|
||||
) -> crate::Result<Child> {
|
||||
let state = State::get().await.unwrap();
|
||||
let settings = state.settings.read().await;
|
||||
let profile = get(path).await?.ok_or_else(|| {
|
||||
crate::Error::OtherError(format!(
|
||||
"Tried to run a nonexistent or unloaded profile at path {}!",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
let version = state
|
||||
.metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == profile.metadata.game_version.as_ref())
|
||||
.ok_or_else(|| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"Invalid or unknown Minecraft version: {}",
|
||||
profile.metadata.game_version
|
||||
))
|
||||
})?;
|
||||
let version_info = d::minecraft::fetch_version_info(version).await?;
|
||||
|
||||
let ref pre_launch_hooks =
|
||||
profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
|
||||
for hook in pre_launch_hooks.iter() {
|
||||
// TODO: hook parameters
|
||||
let mut cmd = hook.split(' ');
|
||||
let result = Command::new(cmd.next().unwrap())
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(path)
|
||||
.spawn()?
|
||||
.wait()
|
||||
.await?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
"Non-zero exit code for pre-launch hook: {}",
|
||||
result.code().unwrap_or(-1)
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let java_install = match profile.java {
|
||||
Some(JavaSettings {
|
||||
install: Some(ref install),
|
||||
..
|
||||
}) => install,
|
||||
_ => if version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.filter(|it| it.major_version >= 16)
|
||||
.is_some()
|
||||
{
|
||||
settings.java_17_path.as_ref()
|
||||
} else {
|
||||
settings.java_8_path.as_ref()
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"No Java installed for version {}",
|
||||
version_info.java_version.map_or(8, |it| it.major_version),
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
if !java_install.exists() {
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
"Could not find Java install: {}",
|
||||
java_install.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let ref java_args = profile
|
||||
.java
|
||||
.as_ref()
|
||||
.and_then(|it| it.extra_arguments.as_ref())
|
||||
.unwrap_or(&settings.custom_java_args);
|
||||
|
||||
let wrapper = profile
|
||||
.hooks
|
||||
.as_ref()
|
||||
.map_or(&settings.hooks.wrapper, |it| &it.wrapper);
|
||||
|
||||
let ref memory = profile.memory.unwrap_or(settings.memory);
|
||||
let ref resolution = profile.resolution.unwrap_or(settings.game_resolution);
|
||||
|
||||
crate::launcher::launch_minecraft(
|
||||
&profile.metadata.game_version,
|
||||
&profile.metadata.loader_version,
|
||||
&profile.path,
|
||||
&java_install,
|
||||
&java_args,
|
||||
&wrapper,
|
||||
memory,
|
||||
resolution,
|
||||
credentials,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn kill(running: &mut Child) -> crate::Result<()> {
|
||||
running.kill().await?;
|
||||
wait_for(running).await
|
||||
}
|
||||
|
||||
pub async fn wait_for(running: &mut Child) -> crate::Result<()> {
|
||||
let result = running.wait().await.map_err(|err| {
|
||||
crate::Error::LauncherError(format!("Error running minecraft: {err}"))
|
||||
})?;
|
||||
|
||||
match result.success() {
|
||||
false => Err(crate::Error::LauncherError(format!(
|
||||
"Minecraft exited with non-zero code {}",
|
||||
result.code().unwrap_or(-1)
|
||||
))),
|
||||
true => Ok(()),
|
||||
}
|
||||
}
|
||||
22
theseus/src/config.rs
Normal file
22
theseus/src/config.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
//! Configuration structs
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time;
|
||||
|
||||
pub static BINCODE_CONFIG: Lazy<bincode::config::Configuration> =
|
||||
Lazy::new(|| {
|
||||
bincode::config::standard()
|
||||
.with_little_endian()
|
||||
.with_no_limit()
|
||||
});
|
||||
|
||||
pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub fn sled_config() -> sled::Config {
|
||||
sled::Config::default().use_compression(true)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use crate::{data::DataError, LAUNCHER_WORK_DIR};
|
||||
use once_cell::sync;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
const META_FILE: &str = "meta.json";
|
||||
const META_URL: &str = "https://meta.modrinth.com/gamedata";
|
||||
|
||||
static METADATA: sync::OnceCell<RwLock<Metadata>> = sync::OnceCell::new();
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Metadata {
|
||||
pub minecraft: daedalus::minecraft::VersionManifest,
|
||||
pub forge: daedalus::modded::Manifest,
|
||||
pub fabric: daedalus::modded::Manifest,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub async fn init() -> Result<(), DataError> {
|
||||
let meta_path = LAUNCHER_WORK_DIR.join(META_FILE);
|
||||
|
||||
if meta_path.exists() {
|
||||
let meta_data = tokio::fs::read_to_string(meta_path)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|it| serde_json::from_str::<Metadata>(&it).ok());
|
||||
|
||||
if let Some(metadata) = meta_data {
|
||||
METADATA.get_or_init(|| RwLock::new(metadata));
|
||||
}
|
||||
}
|
||||
|
||||
let future = async {
|
||||
for attempt in 0..=3 {
|
||||
let res = async {
|
||||
let new = Self::fetch().await?;
|
||||
|
||||
std::fs::write(
|
||||
LAUNCHER_WORK_DIR.join(META_FILE),
|
||||
&serde_json::to_string(&new)?,
|
||||
)?;
|
||||
|
||||
if let Some(metadata) = METADATA.get() {
|
||||
*metadata.write().await = new;
|
||||
} else {
|
||||
METADATA.get_or_init(|| RwLock::new(new));
|
||||
}
|
||||
|
||||
Ok::<(), DataError>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
break;
|
||||
}
|
||||
Err(_) if attempt <= 3 => continue,
|
||||
Err(err) => {
|
||||
log::warn!("Unable to fetch launcher metadata: {}", err)
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if METADATA.get().is_some() {
|
||||
tokio::task::spawn(future);
|
||||
} else {
|
||||
future.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch() -> Result<Self, DataError> {
|
||||
let (game, forge, fabric) = futures::future::join3(
|
||||
daedalus::minecraft::fetch_version_manifest(Some(&format!(
|
||||
"{}/minecraft/v0/manifest.json",
|
||||
META_URL
|
||||
))),
|
||||
daedalus::modded::fetch_manifest(&format!(
|
||||
"{}/forge/v0/manifest.json",
|
||||
META_URL
|
||||
)),
|
||||
daedalus::modded::fetch_manifest(&format!(
|
||||
"{}/fabric/v0/manifest.json",
|
||||
META_URL
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Self {
|
||||
minecraft: game?,
|
||||
forge: forge?,
|
||||
fabric: fabric?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||
let res = METADATA
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("metadata".to_string()))?
|
||||
.read()
|
||||
.await;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
pub use meta::Metadata;
|
||||
pub use profiles::{Profile, Profiles};
|
||||
pub use settings::Settings;
|
||||
|
||||
mod meta;
|
||||
pub mod profiles;
|
||||
mod settings;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum DataError {
|
||||
#[error("I/O error while reading data: {0}")]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error("Daedalus error: {0}")]
|
||||
DaedalusError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Data format error: {0}")]
|
||||
FormatError(String),
|
||||
|
||||
#[error("Attempted to access {0} without initializing it!")]
|
||||
InitializedError(String),
|
||||
|
||||
#[error("Error while serializing/deserializing data")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
use super::DataError;
|
||||
use crate::launcher::ModLoader;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use futures::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
process::{Child, Command},
|
||||
sync::{Mutex, RwLock, RwLockReadGuard},
|
||||
};
|
||||
|
||||
static PROFILES: OnceCell<RwLock<Profiles>> = OnceCell::new();
|
||||
pub const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profiles(pub HashMap<PathBuf, Profile>);
|
||||
|
||||
// TODO: possibly add defaults to some of these values
|
||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
pub const SUPPORTED_ICON_FORMATS: &[&'static str] = &[
|
||||
"bmp", "gif", "jpeg", "jpg", "jpe", "png", "svg", "svgz", "webp", "rgb",
|
||||
"mp4",
|
||||
];
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub metadata: Metadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<MemorySettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<WindowSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hooks: Option<ProfileHooks>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<PathBuf>,
|
||||
pub game_version: String,
|
||||
#[serde(default)]
|
||||
pub loader: ModLoader,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loader_version: Option<LoaderVersion>,
|
||||
pub format_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct JavaSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub install: Option<PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_arguments: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub struct MemorySettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<u32>,
|
||||
pub maximum: u32,
|
||||
}
|
||||
|
||||
impl Default for MemorySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
minimum: None,
|
||||
maximum: 2048,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub struct WindowSize(pub u16, pub u16);
|
||||
|
||||
impl Default for WindowSize {
|
||||
fn default() -> Self {
|
||||
Self(854, 480)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ProfileHooks {
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty", default)]
|
||||
pub pre_launch: HashSet<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty", default)]
|
||||
pub post_exit: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for ProfileHooks {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pre_launch: HashSet::<String>::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::<String>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub async fn new(
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> Result<Self, DataError> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(DataError::FormatError(String::from(
|
||||
"Empty name for instance!",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path: path.canonicalize()?,
|
||||
metadata: Metadata {
|
||||
name,
|
||||
icon: None,
|
||||
game_version: version,
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: None,
|
||||
memory: None,
|
||||
resolution: None,
|
||||
hooks: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
&self,
|
||||
credentials: &crate::launcher::Credentials,
|
||||
) -> Result<Child, crate::launcher::LauncherError> {
|
||||
use crate::launcher::LauncherError;
|
||||
let (settings, version_info) = tokio::try_join! {
|
||||
super::Settings::get(),
|
||||
super::Metadata::get()
|
||||
.and_then(|manifest| async move {
|
||||
let version = manifest
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == self.metadata.game_version.as_ref())
|
||||
.ok_or_else(|| DataError::FormatError(format!(
|
||||
"invalid or unknown version: {}",
|
||||
self.metadata.game_version
|
||||
)))?;
|
||||
|
||||
Ok(daedalus::minecraft::fetch_version_info(version)
|
||||
.await?)
|
||||
})
|
||||
}?;
|
||||
|
||||
let ref pre_launch_hooks =
|
||||
self.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
|
||||
for hook in pre_launch_hooks.iter() {
|
||||
// TODO: hook parameters
|
||||
let mut cmd = hook.split(' ');
|
||||
let result = Command::new(cmd.next().unwrap())
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(&self.path)
|
||||
.spawn()?
|
||||
.wait()
|
||||
.await?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(LauncherError::ExitError(
|
||||
result.code().unwrap_or(-1),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let java_install = match self.java {
|
||||
Some(JavaSettings {
|
||||
install: Some(ref install),
|
||||
..
|
||||
}) => install,
|
||||
_ => if version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.filter(|it| it.major_version >= 16)
|
||||
.is_some()
|
||||
{
|
||||
settings.java_17_path.as_ref()
|
||||
} else {
|
||||
settings.java_8_path.as_ref()
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
LauncherError::JavaError(format!(
|
||||
"No Java installed for version {}",
|
||||
version_info.java_version.map_or(8, |it| it.major_version),
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
if !java_install.exists() {
|
||||
return Err(LauncherError::JavaError(format!(
|
||||
"Could not find java install: {}",
|
||||
java_install.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let java_args = &self
|
||||
.java
|
||||
.as_ref()
|
||||
.and_then(|it| it.extra_arguments.as_ref())
|
||||
.unwrap_or(&settings.custom_java_args);
|
||||
|
||||
let wrapper = self
|
||||
.hooks
|
||||
.as_ref()
|
||||
.map_or(&settings.hooks.wrapper, |it| &it.wrapper);
|
||||
|
||||
let ref memory = self.memory.unwrap_or(settings.memory);
|
||||
let ref resolution =
|
||||
self.resolution.unwrap_or(settings.game_resolution);
|
||||
|
||||
crate::launcher::launch_minecraft(
|
||||
&self.metadata.game_version,
|
||||
&self.metadata.loader_version,
|
||||
&self.path,
|
||||
&java_install,
|
||||
&java_args,
|
||||
&wrapper,
|
||||
memory,
|
||||
resolution,
|
||||
credentials,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn kill(
|
||||
&self,
|
||||
running: &mut Child,
|
||||
) -> Result<(), crate::launcher::LauncherError> {
|
||||
running.kill().await?;
|
||||
self.wait_for(running).await
|
||||
}
|
||||
|
||||
pub async fn wait_for(
|
||||
&self,
|
||||
running: &mut Child,
|
||||
) -> Result<(), crate::launcher::LauncherError> {
|
||||
let result = running.wait().await.map_err(|err| {
|
||||
crate::launcher::LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: String::from("minecraft"),
|
||||
}
|
||||
})?;
|
||||
|
||||
match result.success() {
|
||||
false => Err(crate::launcher::LauncherError::ExitError(
|
||||
result.code().unwrap_or(-1),
|
||||
)),
|
||||
true => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: deduplicate these builder methods
|
||||
// They are flat like this in order to allow builder-style usage
|
||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||
self.metadata.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn with_icon(
|
||||
&mut self,
|
||||
icon: &Path,
|
||||
) -> Result<&mut Self, DataError> {
|
||||
let ext = icon
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("");
|
||||
|
||||
if SUPPORTED_ICON_FORMATS.contains(&ext) {
|
||||
let file_name = format!("icon.{ext}");
|
||||
fs::copy(icon, &self.path.join(&file_name)).await?;
|
||||
self.metadata.icon =
|
||||
Some(Path::new(&format!("./{file_name}")).to_owned());
|
||||
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(DataError::FormatError(format!(
|
||||
"Unsupported image type: {ext}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||
self.metadata.game_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_loader(
|
||||
&mut self,
|
||||
loader: ModLoader,
|
||||
version: Option<LoaderVersion>,
|
||||
) -> &mut Self {
|
||||
self.metadata.loader = loader;
|
||||
self.metadata.loader_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_java_settings(
|
||||
&mut self,
|
||||
settings: Option<JavaSettings>,
|
||||
) -> &mut Self {
|
||||
self.java = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_memory(
|
||||
&mut self,
|
||||
settings: Option<MemorySettings>,
|
||||
) -> &mut Self {
|
||||
self.memory = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_resolution(
|
||||
&mut self,
|
||||
resolution: Option<WindowSize>,
|
||||
) -> &mut Self {
|
||||
self.resolution = resolution;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hooks(&mut self, hooks: Option<ProfileHooks>) -> &mut Self {
|
||||
self.hooks = hooks;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
pub async fn init() -> Result<(), DataError> {
|
||||
let settings = super::Settings::get().await?;
|
||||
let profiles = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let futures = settings.profiles.clone().into_iter().map(|path| async {
|
||||
let profiles = Arc::clone(&profiles);
|
||||
tokio::spawn(async move {
|
||||
// TODO: handle missing profiles
|
||||
let mut profiles = profiles.lock().await;
|
||||
let profile = Self::read_profile_from_dir(path.clone()).await?;
|
||||
|
||||
profiles.insert(path, profile);
|
||||
Ok(()) as Result<_, DataError>
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
futures::future::try_join_all(futures).await?;
|
||||
|
||||
PROFILES.get_or_init(|| {
|
||||
RwLock::new(Profiles(
|
||||
Arc::try_unwrap(profiles).unwrap().into_inner(),
|
||||
))
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert(profile: Profile) -> Result<(), DataError> {
|
||||
let mut profiles = PROFILES
|
||||
.get()
|
||||
.ok_or_else(|| {
|
||||
DataError::InitializedError(String::from("profiles"))
|
||||
})?
|
||||
.write()
|
||||
.await;
|
||||
|
||||
super::Settings::get_mut()
|
||||
.await?
|
||||
.profiles
|
||||
.insert(profile.path.clone());
|
||||
profiles.0.insert(profile.path.clone(), profile);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_from(path: PathBuf) -> Result<(), DataError> {
|
||||
Self::read_profile_from_dir(path)
|
||||
.and_then(Self::insert)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove(path: &Path) -> Result<Option<Profile>, DataError> {
|
||||
let path = path.canonicalize()?;
|
||||
let mut profiles = PROFILES.get().unwrap().write().await;
|
||||
super::Settings::get_mut().await?.profiles.remove(&path);
|
||||
Ok(profiles.0.remove(&path))
|
||||
}
|
||||
|
||||
pub async fn save() -> Result<(), DataError> {
|
||||
let profiles = Self::get().await?;
|
||||
|
||||
let futures = profiles.0.clone().into_iter().map(|(path, profile)| {
|
||||
tokio::spawn(async move {
|
||||
let json = tokio::task::spawn_blocking(move || {
|
||||
serde_json::to_vec_pretty(&profile)
|
||||
})
|
||||
.await
|
||||
.unwrap()?;
|
||||
|
||||
let profile_json_path = path.join(PROFILE_JSON_PATH);
|
||||
fs::write(profile_json_path, json).await?;
|
||||
Ok(()) as Result<(), DataError>
|
||||
})
|
||||
});
|
||||
futures::future::try_join_all(futures)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.collect::<Result<_, DataError>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||
Ok(PROFILES
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("profiles".to_string()))?
|
||||
.read()
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(
|
||||
path: PathBuf,
|
||||
) -> Result<Profile, DataError> {
|
||||
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = path.clone();
|
||||
Ok(profile)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
#[test]
|
||||
fn profile_test() -> Result<(), serde_json::Error> {
|
||||
let profile = Profile {
|
||||
path: PathBuf::from("/tmp/nunya/beeswax"),
|
||||
metadata: Metadata {
|
||||
name: String::from("Example Pack"),
|
||||
icon: None,
|
||||
game_version: String::from("1.18.2"),
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: JavaSettings {
|
||||
install: PathBuf::from("/usr/bin/java"),
|
||||
extra_arguments: Vec::new(),
|
||||
},
|
||||
memory: MemorySettings {
|
||||
minimum: None,
|
||||
maximum: 8192,
|
||||
},
|
||||
resolution: WindowSize(1920, 1080),
|
||||
hooks: ProfileHooks {
|
||||
pre_launch: HashSet::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::new(),
|
||||
},
|
||||
};
|
||||
let json = serde_json::json!({
|
||||
"path": "/tmp/nunya/beeswax",
|
||||
"metadata": {
|
||||
"name": "Example Pack",
|
||||
"game_version": "1.18.2",
|
||||
"format_version": 1u32,
|
||||
},
|
||||
"java": {
|
||||
"install": "/usr/bin/java",
|
||||
},
|
||||
"memory": {
|
||||
"maximum": 8192u32,
|
||||
},
|
||||
"resolution": (1920u16, 1080u16),
|
||||
"hooks": {},
|
||||
});
|
||||
|
||||
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
|
||||
assert_str_eq!(
|
||||
format!("{:?}", serde_json::from_value::<Profile>(json)?),
|
||||
format!("{:?}", profile),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
use super::profiles::*;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use crate::{data::DataError, LAUNCHER_WORK_DIR};
|
||||
use once_cell::sync;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
const SETTINGS_FILE: &str = "settings.json";
|
||||
const ICONS_PATH: &str = "icons";
|
||||
const METADATA_DIR: &str = "meta";
|
||||
const SETTINGS_PATH_ENV: &str = "THESEUS_CONFIG_DIR";
|
||||
|
||||
static SETTINGS: sync::OnceCell<RwLock<Settings>> = sync::OnceCell::new();
|
||||
pub const FORMAT_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub memory: MemorySettings,
|
||||
pub game_resolution: WindowSize,
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub java_8_path: Option<PathBuf>,
|
||||
pub java_17_path: Option<PathBuf>,
|
||||
pub hooks: ProfileHooks,
|
||||
pub icon_path: PathBuf,
|
||||
pub metadata_dir: PathBuf,
|
||||
pub profiles: HashSet<PathBuf>,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
java_8_path: None,
|
||||
java_17_path: None,
|
||||
hooks: ProfileHooks::default(),
|
||||
icon_path: LAUNCHER_WORK_DIR.join(ICONS_PATH),
|
||||
metadata_dir: LAUNCHER_WORK_DIR.join(METADATA_DIR),
|
||||
profiles: HashSet::new(),
|
||||
max_concurrent_downloads: 32,
|
||||
version: FORMAT_VERSION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub async fn init() -> Result<(), DataError> {
|
||||
let settings_path = std::env::var_os(SETTINGS_PATH_ENV)
|
||||
.map_or(LAUNCHER_WORK_DIR.join(SETTINGS_FILE), PathBuf::from);
|
||||
|
||||
if settings_path.exists() {
|
||||
let settings_data = tokio::fs::read_to_string(settings_path)
|
||||
.await
|
||||
.map(|x| serde_json::from_str::<Settings>(&x).ok())
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some(settings) = settings_data {
|
||||
SETTINGS.get_or_init(|| RwLock::new(settings));
|
||||
}
|
||||
}
|
||||
|
||||
if SETTINGS.get().is_none() {
|
||||
let new = Self::default();
|
||||
|
||||
tokio::fs::write(
|
||||
LAUNCHER_WORK_DIR.join(SETTINGS_FILE),
|
||||
&serde_json::to_string(&new)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
SETTINGS.get_or_init(|| RwLock::new(new));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load() -> Result<(), DataError> {
|
||||
let new = serde_json::from_str::<Settings>(&std::fs::read_to_string(
|
||||
LAUNCHER_WORK_DIR.join(SETTINGS_FILE),
|
||||
)?)?;
|
||||
|
||||
let mut write = SETTINGS
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))?
|
||||
.write()
|
||||
.await;
|
||||
|
||||
*write = new;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save() -> Result<(), DataError> {
|
||||
let settings = Self::get().await?;
|
||||
|
||||
std::fs::write(
|
||||
LAUNCHER_WORK_DIR.join(SETTINGS_FILE),
|
||||
&serde_json::to_string_pretty(&*settings)?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||
Ok(Self::get_or_uninit::<'a>()?.read().await)
|
||||
}
|
||||
|
||||
pub async fn get_mut<'a>() -> Result<RwLockWriteGuard<'a, Self>, DataError>
|
||||
{
|
||||
Ok(Self::get_or_uninit::<'a>()?.write().await)
|
||||
}
|
||||
|
||||
fn get_or_uninit<'a>() -> Result<&'a RwLock<Self>, DataError> {
|
||||
SETTINGS
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))
|
||||
}
|
||||
}
|
||||
55
theseus/src/error.rs
Normal file
55
theseus/src/error.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Theseus error type
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Filesystem error: {0}")]
|
||||
FSError(String),
|
||||
|
||||
#[error("Serialization error (JSON): {0}")]
|
||||
JSONError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Serialization error (Bincode): {0}")]
|
||||
EncodeError(#[from] bincode::error::DecodeError),
|
||||
|
||||
#[error("Deserialization error (Bincode): {0}")]
|
||||
DecodeError(#[from] bincode::error::EncodeError),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
DBError(#[from] sled::Error),
|
||||
|
||||
#[error("Unable to read {0} from any source")]
|
||||
NoValueFor(String),
|
||||
|
||||
#[error("Metadata error: {0}")]
|
||||
MetadataError(#[from] daedalus::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("Error launching Minecraft: {0}")]
|
||||
LauncherError(String),
|
||||
|
||||
#[error("Error fetching URL: {0}")]
|
||||
FetchError(#[from] reqwest::Error),
|
||||
|
||||
#[error("Incorrect Sha1 hash for download: {0} != {1}")]
|
||||
HashError(String, String),
|
||||
|
||||
#[error("Paths stored in the database need to be valid UTF-8: {0}")]
|
||||
UTFError(std::path::PathBuf),
|
||||
|
||||
#[error("Invalid input: {0}")]
|
||||
InputError(String),
|
||||
|
||||
#[error(
|
||||
"Tried to access unloaded profile {0}, loading it probably failed"
|
||||
)]
|
||||
UnloadedProfileError(String),
|
||||
|
||||
#[error("Profile {0} is not managed by Theseus!")]
|
||||
UnmanagedProfileError(String),
|
||||
|
||||
#[error("Error: {0}")]
|
||||
OtherError(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
@@ -1,32 +1,29 @@
|
||||
use crate::data::profiles::*;
|
||||
use crate::launcher::auth::provider::Credentials;
|
||||
use crate::launcher::rules::parse_rules;
|
||||
use crate::launcher::LauncherError;
|
||||
use daedalus::get_path_from_artifact;
|
||||
use daedalus::minecraft::{Argument, ArgumentValue, Library, Os, VersionType};
|
||||
use daedalus::modded::SidedDataEntry;
|
||||
use std::collections::HashMap;
|
||||
//! Minecraft CLI argument logic
|
||||
// TODO: Rafactor this section
|
||||
use super::{auth::Credentials, parse_rule};
|
||||
use crate::{
|
||||
state::{MemorySettings, WindowSize},
|
||||
util::platform::classpath_separator,
|
||||
};
|
||||
use daedalus::{
|
||||
get_path_from_artifact,
|
||||
minecraft::{Argument, ArgumentValue, Library, VersionType},
|
||||
modded::SidedDataEntry,
|
||||
};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn get_cp_separator() -> &'static str {
|
||||
match super::download::get_os() {
|
||||
Os::Osx | Os::Linux | Os::Unknown => ":",
|
||||
Os::Windows => ";",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_class_paths(
|
||||
libraries_path: &Path,
|
||||
libraries: &[Library],
|
||||
client_path: &Path,
|
||||
) -> Result<String, LauncherError> {
|
||||
let mut class_paths = libraries
|
||||
) -> crate::Result<String> {
|
||||
let mut cps = libraries
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !super::rules::parse_rules(rules.as_slice()) {
|
||||
if !rules.iter().all(parse_rule) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -39,10 +36,11 @@ pub fn get_class_paths(
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
class_paths.push(
|
||||
crate::util::absolute_path(&client_path)
|
||||
cps.push(
|
||||
client_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
client_path.to_string_lossy()
|
||||
))
|
||||
@@ -51,44 +49,35 @@ pub fn get_class_paths(
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Ok(class_paths.join(get_cp_separator()))
|
||||
Ok(cps.join(classpath_separator()))
|
||||
}
|
||||
|
||||
pub fn get_class_paths_jar<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
) -> Result<String, LauncherError> {
|
||||
let class_paths = libraries
|
||||
) -> crate::Result<String> {
|
||||
let cps = libraries
|
||||
.iter()
|
||||
.map(|library| get_lib_path(libraries_path, library.as_ref()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(class_paths.join(get_cp_separator()))
|
||||
Ok(cps.join(classpath_separator()))
|
||||
}
|
||||
|
||||
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> Result<String, LauncherError> {
|
||||
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
||||
let mut path = libraries_path.to_path_buf();
|
||||
|
||||
path.push(get_path_from_artifact(lib.as_ref())?);
|
||||
|
||||
let path = crate::util::absolute_path(&path).map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
let path = &path.canonicalize().map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"Library file at path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
})?;
|
||||
|
||||
/*if !path.exists() {
|
||||
if let Some(parent) = &path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
std::fs::File::create(&path)?;
|
||||
}*/
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
pub fn get_jvm_arguments(
|
||||
arguments: Option<&[Argument]>,
|
||||
natives_path: &Path,
|
||||
@@ -97,7 +86,7 @@ pub fn get_jvm_arguments(
|
||||
version_name: &str,
|
||||
memory: MemorySettings,
|
||||
custom_args: Vec<String>,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
if let Some(args) = arguments {
|
||||
@@ -113,8 +102,9 @@ pub fn get_jvm_arguments(
|
||||
} else {
|
||||
parsed_arguments.push(format!(
|
||||
"-Djava.library.path={}",
|
||||
&crate::util::absolute_path(natives_path)
|
||||
.map_err(|_| LauncherError::InvalidInput(format!(
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| crate::Error::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
)))?
|
||||
@@ -144,14 +134,15 @@ fn parse_jvm_argument(
|
||||
libraries_path: &Path,
|
||||
class_paths: &str,
|
||||
version_name: &str,
|
||||
) -> Result<String, LauncherError> {
|
||||
) -> crate::Result<String> {
|
||||
argument.retain(|c| !c.is_whitespace());
|
||||
Ok(argument
|
||||
.replace(
|
||||
"${natives_directory}",
|
||||
&crate::util::absolute_path(natives_path)
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
@@ -160,9 +151,10 @@ fn parse_jvm_argument(
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
&crate::util::absolute_path(libraries_path)
|
||||
&libraries_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified libraries path {} does not exist",
|
||||
libraries_path.to_string_lossy()
|
||||
))
|
||||
@@ -170,7 +162,7 @@ fn parse_jvm_argument(
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
.replace("${classpath_separator}", get_cp_separator())
|
||||
.replace("${classpath_separator}", classpath_separator())
|
||||
.replace("${launcher_name}", "theseus")
|
||||
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
|
||||
.replace("${version_name}", version_name)
|
||||
@@ -188,7 +180,7 @@ pub fn get_minecraft_arguments(
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
) -> crate::Result<Vec<String>> {
|
||||
if let Some(arguments) = arguments {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
@@ -242,7 +234,7 @@ fn parse_minecraft_argument(
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
) -> Result<String, LauncherError> {
|
||||
) -> crate::Result<String> {
|
||||
Ok(argument
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
@@ -254,9 +246,10 @@ fn parse_minecraft_argument(
|
||||
.replace("${assets_index_name}", asset_index_name)
|
||||
.replace(
|
||||
"${game_directory}",
|
||||
&crate::util::absolute_path(game_directory)
|
||||
&game_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified game directory {} does not exist",
|
||||
game_directory.to_string_lossy()
|
||||
))
|
||||
@@ -266,9 +259,10 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
&crate::util::absolute_path(assets_directory)
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
@@ -278,9 +272,10 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
&crate::util::absolute_path(assets_directory)
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
@@ -297,9 +292,9 @@ fn parse_arguments<F>(
|
||||
arguments: &[Argument],
|
||||
parsed_arguments: &mut Vec<String>,
|
||||
parse_function: F,
|
||||
) -> Result<(), LauncherError>
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(&str) -> Result<String, LauncherError>,
|
||||
F: Fn(&str) -> crate::Result<String>,
|
||||
{
|
||||
for argument in arguments {
|
||||
match argument {
|
||||
@@ -311,7 +306,7 @@ where
|
||||
}
|
||||
}
|
||||
Argument::Ruled { rules, value } => {
|
||||
if parse_rules(rules.as_slice()) {
|
||||
if rules.iter().all(parse_rule) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed_arguments.push(parse_function(arg)?);
|
||||
@@ -334,7 +329,7 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
arguments: &[T],
|
||||
data: &HashMap<String, SidedDataEntry>,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut new_arguments = Vec::new();
|
||||
|
||||
for argument in arguments {
|
||||
@@ -342,7 +337,10 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
if argument.as_ref().starts_with('{') {
|
||||
if let Some(entry) = data.get(trimmed_arg) {
|
||||
new_arguments.push(if entry.client.starts_with('[') {
|
||||
get_lib_path(libraries_path, &entry.client[1..entry.client.len() - 1])?
|
||||
get_lib_path(
|
||||
libraries_path,
|
||||
&entry.client[1..entry.client.len() - 1],
|
||||
)?
|
||||
} else {
|
||||
entry.client.clone()
|
||||
})
|
||||
@@ -357,15 +355,23 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
Ok(new_arguments)
|
||||
}
|
||||
|
||||
pub async fn get_processor_main_class(path: String) -> Result<Option<String>, LauncherError> {
|
||||
pub async fn get_processor_main_class(
|
||||
path: String,
|
||||
) -> crate::Result<Option<String>> {
|
||||
Ok(tokio::task::spawn_blocking(move || {
|
||||
let zipfile = std::fs::File::open(&path)?;
|
||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||
LauncherError::ProcessorError(format!("Cannot read processor at {}", path))
|
||||
crate::Error::LauncherError(format!(
|
||||
"Cannot read processor at {}",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
|
||||
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
||||
LauncherError::ProcessorError(format!("Cannot read processor manifest at {}", path))
|
||||
crate::Error::LauncherError(format!(
|
||||
"Cannot read processor manifest at {}",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
@@ -381,7 +387,8 @@ pub async fn get_processor_main_class(path: String) -> Result<Option<String>, La
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<Option<String>, LauncherError>(None)
|
||||
Ok::<Option<String>, crate::Error>(None)
|
||||
})
|
||||
.await??)
|
||||
.await
|
||||
.unwrap()?)
|
||||
}
|
||||
|
||||
@@ -1,205 +1,8 @@
|
||||
pub mod api {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GameProfile {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserProperty {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub properties: Option<Vec<UserProperty>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthenticateResponse {
|
||||
pub user: Option<User>,
|
||||
pub client_token: Uuid,
|
||||
pub access_token: String,
|
||||
pub available_profiles: Vec<GameProfile>,
|
||||
pub selected_profile: Option<GameProfile>,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
username: &str,
|
||||
password: &str,
|
||||
request_user: bool,
|
||||
) -> Result<AuthenticateResponse, reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/authenticate")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"agent": {
|
||||
"name": "Minecraft",
|
||||
"version": 1
|
||||
},
|
||||
"username": username,
|
||||
"password": password,
|
||||
"clientToken": Uuid::new_v4(),
|
||||
"requestUser": request_user
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn sign_out(username: &str, password: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/signout")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn validate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/validate")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn invalidate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/invalidate")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefreshResponse {
|
||||
pub user: Option<User>,
|
||||
pub client_token: Uuid,
|
||||
pub access_token: String,
|
||||
pub selected_profile: Option<GameProfile>,
|
||||
}
|
||||
|
||||
pub async fn refresh(
|
||||
access_token: &str,
|
||||
client_token: &str,
|
||||
selected_profile: &GameProfile,
|
||||
request_user: bool,
|
||||
) -> Result<RefreshResponse, reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/refresh")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token,
|
||||
"selectedProfile": {
|
||||
"id": selected_profile.id,
|
||||
"name": selected_profile.name,
|
||||
},
|
||||
"requestUser": request_user,
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub mod provider {
|
||||
use crate::launcher::auth::api::login;
|
||||
use crate::launcher::LauncherError;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// The credentials of a user
|
||||
pub struct Credentials {
|
||||
/// The user UUID the credentials belong to
|
||||
pub id: Uuid,
|
||||
/// The username of the user
|
||||
pub username: String,
|
||||
/// The access token associated with the credentials
|
||||
pub access_token: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Gets a credentials instance from a user's login
|
||||
pub async fn from_login(username: &str, password: &str) -> Result<Self, LauncherError> {
|
||||
let login =
|
||||
login(username, password, true)
|
||||
.await
|
||||
.map_err(|err| LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: "authentication credentials".to_string(),
|
||||
})?;
|
||||
|
||||
let profile = login.selected_profile.unwrap();
|
||||
|
||||
Ok(Credentials {
|
||||
id: profile.id,
|
||||
username: profile.name,
|
||||
access_token: login.access_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
//! Authentication flow
|
||||
// TODO: Implement authentication
|
||||
#[derive(Debug)]
|
||||
pub struct Credentials {
|
||||
pub id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub access_token: String,
|
||||
}
|
||||
|
||||
@@ -1,362 +1,282 @@
|
||||
//! Downloader for Minecraft data
|
||||
|
||||
use crate::{
|
||||
data::{DataError, Settings},
|
||||
launcher::LauncherError,
|
||||
state::State,
|
||||
util::{fetch::*, platform::OsExt},
|
||||
};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use daedalus::minecraft::{
|
||||
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType,
|
||||
Library, Os, Version, VersionInfo,
|
||||
};
|
||||
use daedalus::modded::{
|
||||
fetch_partial_version, merge_partial_version, LoaderVersion,
|
||||
};
|
||||
use futures::future;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::AsyncWriteExt,
|
||||
sync::{OnceCell, Semaphore},
|
||||
use daedalus::{
|
||||
self as d,
|
||||
minecraft::{
|
||||
Asset, AssetsIndex, Library, Os, Version as GameVersion,
|
||||
VersionInfo as GameVersionInfo,
|
||||
},
|
||||
modded::LoaderVersion,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use tokio::{fs, sync::OnceCell};
|
||||
|
||||
static DOWNLOADS_SEMAPHORE: OnceCell<Semaphore> = OnceCell::const_new();
|
||||
pub async fn download_minecraft(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
) -> crate::Result<()> {
|
||||
log::info!("Downloading Minecraft version {}", version.id);
|
||||
let assets_index = download_assets_index(st, version).await?;
|
||||
|
||||
pub async fn init() -> Result<(), DataError> {
|
||||
DOWNLOADS_SEMAPHORE
|
||||
.get_or_try_init(|| async {
|
||||
let settings = Settings::get().await?;
|
||||
Ok::<_, DataError>(Semaphore::new(
|
||||
settings.max_concurrent_downloads,
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
tokio::try_join! {
|
||||
download_client(st, version),
|
||||
download_assets(st, version.assets == "legacy", &assets_index),
|
||||
download_libraries(st, version.libraries.as_slice(), &version.id)
|
||||
}?;
|
||||
|
||||
log::info!("Done downloading Minecraft!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_version_info(
|
||||
client_path: &Path,
|
||||
version: &Version,
|
||||
loader_version: Option<&LoaderVersion>,
|
||||
) -> Result<VersionInfo, LauncherError> {
|
||||
let id = match loader_version {
|
||||
Some(x) => &x.id,
|
||||
None => &version.id,
|
||||
};
|
||||
st: &State,
|
||||
version: &GameVersion,
|
||||
loader: Option<&LoaderVersion>,
|
||||
) -> crate::Result<GameVersionInfo> {
|
||||
let version_id = loader.map_or(&version.id, |it| &it.id);
|
||||
log::debug!("Loading version info for Minecraft {version_id}");
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version_id)
|
||||
.join(format!("{version_id}.json"));
|
||||
|
||||
let mut path = client_path.join(id);
|
||||
path.push(&format!("{id}.json"));
|
||||
|
||||
if path.exists() {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&contents)?)
|
||||
let res = if path.exists() {
|
||||
fs::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
let mut info = fetch_version_info(version).await?;
|
||||
log::info!("Downloading version info for version {}", &version.id);
|
||||
let mut info = d::minecraft::fetch_version_info(version).await?;
|
||||
|
||||
if let Some(loader_version) = loader_version {
|
||||
let partial = fetch_partial_version(&loader_version.url).await?;
|
||||
info = merge_partial_version(partial, info);
|
||||
info.id = loader_version.id.clone();
|
||||
if let Some(loader) = loader {
|
||||
let partial = d::modded::fetch_partial_version(&loader.url).await?;
|
||||
info = d::modded::merge_partial_version(partial, info);
|
||||
info.id = loader.id.clone();
|
||||
}
|
||||
let info_s = serde_json::to_string(&info)?;
|
||||
save_file(&path, &bytes::Bytes::from(info_s)).await?;
|
||||
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
write(&path, &serde_json::to_vec(&info)?, &permit).await?;
|
||||
Ok(info)
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Loaded version info for Minecraft {version_id}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn download_client(
|
||||
client_path: &Path,
|
||||
version_info: &VersionInfo,
|
||||
) -> Result<(), LauncherError> {
|
||||
let version = &version_info.id;
|
||||
st: &State,
|
||||
version_info: &GameVersionInfo,
|
||||
) -> crate::Result<()> {
|
||||
let ref version = version_info.id;
|
||||
log::debug!("Locating client for version {version}");
|
||||
let client_download = version_info
|
||||
.downloads
|
||||
.get(&DownloadType::Client)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Version {version} does not have any client downloads"
|
||||
))
|
||||
})?;
|
||||
.get(&d::minecraft::DownloadType::Client)
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
)))?;
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version)
|
||||
.join(format!("{version}.jar"));
|
||||
|
||||
let mut path = client_path.join(version);
|
||||
path.push(&format!("{version}.jar"));
|
||||
if !path.exists() {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let bytes =
|
||||
fetch(&client_download.url, Some(&client_download.sha1), &permit)
|
||||
.await?;
|
||||
write(&path, &bytes, &permit).await?;
|
||||
log::info!("Fetched client version {version}");
|
||||
}
|
||||
|
||||
save_and_download_file(
|
||||
&path,
|
||||
&client_download.url,
|
||||
Some(&client_download.sha1),
|
||||
)
|
||||
.await?;
|
||||
log::debug!("Client loaded for version {version}!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_assets_index(
|
||||
assets_path: &Path,
|
||||
version: &VersionInfo,
|
||||
) -> Result<AssetsIndex, LauncherError> {
|
||||
let path =
|
||||
assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
) -> crate::Result<AssetsIndex> {
|
||||
log::debug!("Loading assets index");
|
||||
let path = st
|
||||
.directories
|
||||
.assets_index_dir()
|
||||
.join(format!("{}.json", &version.asset_index.id));
|
||||
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
let res = if path.exists() {
|
||||
fs::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
let index = fetch_assets_index(version).await?;
|
||||
|
||||
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))
|
||||
.await?;
|
||||
|
||||
let index = d::minecraft::fetch_assets_index(version).await?;
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
write(&path, &serde_json::to_vec(&index)?, &permit).await?;
|
||||
log::info!("Fetched assets index");
|
||||
Ok(index)
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Assets index successfully loaded!");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn download_assets(
|
||||
assets_path: &Path,
|
||||
legacy_path: Option<&Path>,
|
||||
st: &State,
|
||||
with_legacy: bool,
|
||||
index: &AssetsIndex,
|
||||
) -> Result<(), LauncherError> {
|
||||
future::join_all(index.objects.iter().map(|(name, asset)| {
|
||||
download_asset(assets_path, legacy_path, name, asset)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading assets");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
stream::iter(index.objects.iter())
|
||||
.map(Ok::<(&String, &Asset), crate::Error>)
|
||||
.try_for_each_concurrent(None, |(name, asset)| async move {
|
||||
let ref hash = asset.hash;
|
||||
let resource_path = st.directories.object_dir(hash);
|
||||
let url = format!(
|
||||
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
|
||||
sub_hash = &hash[..2]
|
||||
);
|
||||
|
||||
async fn download_asset(
|
||||
assets_path: &Path,
|
||||
legacy_path: Option<&Path>,
|
||||
name: &str,
|
||||
asset: &Asset,
|
||||
) -> Result<(), LauncherError> {
|
||||
let hash = &asset.hash;
|
||||
let sub_hash = &hash[..2];
|
||||
let fetch_cell = OnceCell::<bytes::Bytes>::new();
|
||||
tokio::try_join! {
|
||||
async {
|
||||
if !resource_path.exists() {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
||||
.await?;
|
||||
write(&resource_path, &resource, &permit).await?;
|
||||
log::info!("Fetched asset with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
async {
|
||||
if with_legacy {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
||||
.await?;
|
||||
let resource_path = st.directories.legacy_assets_dir().join(
|
||||
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
||||
);
|
||||
write(&resource_path, &resource, &permit).await?;
|
||||
log::info!("Fetched legacy asset with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
}?;
|
||||
|
||||
let mut resource_path = assets_path.join("objects");
|
||||
resource_path.push(sub_hash);
|
||||
resource_path.push(hash);
|
||||
|
||||
let url =
|
||||
format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
|
||||
|
||||
let resource =
|
||||
save_and_download_file(&resource_path, &url, Some(hash)).await?;
|
||||
|
||||
if let Some(legacy_path) = legacy_path {
|
||||
let resource_path = legacy_path
|
||||
.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
|
||||
save_file(resource_path.as_path(), &resource).await?;
|
||||
}
|
||||
log::debug!("Loaded asset with hash {hash}");
|
||||
Ok(())
|
||||
}).await?;
|
||||
|
||||
log::debug!("Done loading assets!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_libraries(
|
||||
libraries_path: &Path,
|
||||
natives_path: &Path,
|
||||
st: &State,
|
||||
libraries: &[Library],
|
||||
) -> Result<(), LauncherError> {
|
||||
future::join_all(libraries.iter().map(|library| {
|
||||
download_library(libraries_path, natives_path, library)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||
version: &str,
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading libraries");
|
||||
let (libraries_dir, natives_dir) = (
|
||||
Arc::new(st.directories.libraries_dir()),
|
||||
Arc::new(st.directories.version_natives_dir(version)),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
tokio::try_join! {
|
||||
fs::create_dir_all(st.directories.libraries_dir()),
|
||||
fs::create_dir_all(st.directories.version_natives_dir(version))
|
||||
}?;
|
||||
|
||||
async fn download_library(
|
||||
libraries_path: &Path,
|
||||
natives_path: &Path,
|
||||
library: &Library,
|
||||
) -> Result<(), LauncherError> {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !super::rules::parse_rules(rules) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>)
|
||||
.try_for_each_concurrent(None, |library| async move {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !rules.iter().all(super::parse_rule) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = d::get_path_from_artifact(&library.name)?;
|
||||
let path = st.directories.libraries_dir().join(&artifact_path);
|
||||
|
||||
future::try_join(
|
||||
download_library_jar(libraries_path, library),
|
||||
download_native(natives_path, library),
|
||||
)
|
||||
.await?;
|
||||
match library.downloads {
|
||||
_ if path.exists() => Ok(()),
|
||||
Some(d::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact),
|
||||
..
|
||||
}) => {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &permit)
|
||||
.await?;
|
||||
write(&path, &bytes, &permit).await?;
|
||||
log::info!("Fetched library {}", &library.name);
|
||||
Ok::<_, crate::Error>(())
|
||||
}
|
||||
None => {
|
||||
let url = [
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net"),
|
||||
&artifact_path
|
||||
].concat();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let bytes = fetch(&url, None, &permit).await?;
|
||||
write(&path, &bytes, &permit).await?;
|
||||
log::info!("Fetched library {}", &library.name);
|
||||
Ok::<_, crate::Error>(())
|
||||
}
|
||||
_ => Ok(())
|
||||
}
|
||||
},
|
||||
async {
|
||||
// HACK: pseudo try block using or else
|
||||
if let Some((os_key, classifiers)) = None.or_else(|| Some((
|
||||
library
|
||||
.natives
|
||||
.as_ref()?
|
||||
.get(&Os::native())?,
|
||||
library
|
||||
.downloads
|
||||
.as_ref()?
|
||||
.classifiers
|
||||
.as_ref()?
|
||||
))) {
|
||||
let parsed_key = os_key.replace(
|
||||
"${arch}",
|
||||
crate::util::platform::ARCH_WIDTH,
|
||||
);
|
||||
|
||||
async fn download_library_jar(
|
||||
libraries_path: &Path,
|
||||
library: &Library,
|
||||
) -> Result<(), LauncherError> {
|
||||
let artifact_path = get_path_from_artifact(&library.name)?;
|
||||
let path = libraries_path.join(&artifact_path);
|
||||
|
||||
if let Some(downloads) = &library.downloads {
|
||||
if let Some(library) = &downloads.artifact {
|
||||
save_and_download_file(&path, &library.url, Some(&library.sha1))
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
let url = format!(
|
||||
"{}{artifact_path}",
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net/"),
|
||||
);
|
||||
save_and_download_file(&path, &url, None).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_native(
|
||||
natives_path: &Path,
|
||||
library: &Library,
|
||||
) -> Result<(), LauncherError> {
|
||||
use daedalus::minecraft::LibraryDownload;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Try blocks in stable Rust when?
|
||||
let optional_cascade =
|
||||
|| -> Option<(&String, &HashMap<String, LibraryDownload>)> {
|
||||
let os_key = library.natives.as_ref()?.get(&get_os())?;
|
||||
let classifiers =
|
||||
library.downloads.as_ref()?.classifiers.as_ref()?;
|
||||
Some((os_key, classifiers))
|
||||
};
|
||||
|
||||
if let Some((os_key, classifiers)) = optional_cascade() {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
let parsed_key = os_key.replace("${arch}", "64");
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
let parsed_key = os_key.replace("${arch}", "32");
|
||||
|
||||
if let Some(native) = classifiers.get(&parsed_key) {
|
||||
let file = download_file(&native.url, Some(&native.sha1)).await?;
|
||||
|
||||
let reader = std::io::Cursor::new(&file);
|
||||
|
||||
let mut archive = zip::ZipArchive::new(reader).unwrap();
|
||||
archive.extract(natives_path).unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_and_download_file(
|
||||
path: &Path,
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
) -> Result<bytes::Bytes, LauncherError> {
|
||||
match std::fs::read(path) {
|
||||
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
|
||||
Err(_) => {
|
||||
let file = download_file(url, sha1).await?;
|
||||
save_file(path, &file).await?;
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
|
||||
let _save_permit = DOWNLOADS_SEMAPHORE
|
||||
.get()
|
||||
.expect("File operation semaphore not initialized!")
|
||||
.acquire()
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut file = File::create(path).await?;
|
||||
file.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_os() -> Os {
|
||||
match std::env::consts::OS {
|
||||
"windows" => Os::Windows,
|
||||
"macos" => Os::Osx,
|
||||
"linux" => Os::Linux,
|
||||
_ => Os::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_file(
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
) -> Result<bytes::Bytes, LauncherError> {
|
||||
let _download_permit = DOWNLOADS_SEMAPHORE
|
||||
.get()
|
||||
.expect("File operation semaphore not initialized!")
|
||||
.acquire()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(Duration::from_secs(10)))
|
||||
.build()
|
||||
.map_err(|err| LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
})?;
|
||||
|
||||
for attempt in 1..=4 {
|
||||
let result = client.get(url).send().await;
|
||||
|
||||
match result {
|
||||
Ok(x) => {
|
||||
let bytes = x.bytes().await;
|
||||
|
||||
if let Ok(bytes) = bytes {
|
||||
if let Some(sha1) = sha1 {
|
||||
if &get_hash(bytes.clone()).await? != sha1 {
|
||||
if attempt <= 3 {
|
||||
continue;
|
||||
} else {
|
||||
return Err(LauncherError::ChecksumFailure {
|
||||
hash: sha1.to_string(),
|
||||
url: url.to_string(),
|
||||
tries: attempt,
|
||||
});
|
||||
}
|
||||
if let Some(native) = classifiers.get(&parsed_key) {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let data = fetch(&native.url, Some(&native.sha1), &permit).await?;
|
||||
let reader = std::io::Cursor::new(&data);
|
||||
let mut archive = zip::ZipArchive::new(reader).unwrap();
|
||||
archive.extract(&st.directories.version_natives_dir(version)).unwrap();
|
||||
log::info!("Fetched native {}", &library.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(bytes);
|
||||
} else if attempt <= 3 {
|
||||
continue;
|
||||
} else if let Err(err) = bytes {
|
||||
return Err(LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Err(_) if attempt <= 3 => continue,
|
||||
Err(err) => {
|
||||
return Err(LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
})
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Loaded library {}", library.name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
).await?;
|
||||
|
||||
/// Computes a checksum of the input bytes
|
||||
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
|
||||
let hash =
|
||||
tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest())
|
||||
.await?;
|
||||
|
||||
Ok(hash)
|
||||
log::debug!("Done loading libraries!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,205 +1,116 @@
|
||||
use daedalus::minecraft::{ArgumentType, VersionInfo};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! Logic for launching Minecraft
|
||||
use crate::state as st;
|
||||
use daedalus as d;
|
||||
use std::{path::Path, process::Stdio};
|
||||
use thiserror::Error;
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
pub use crate::launcher::auth::provider::Credentials;
|
||||
|
||||
mod args;
|
||||
pub mod auth;
|
||||
|
||||
mod auth;
|
||||
pub use auth::Credentials;
|
||||
|
||||
mod download;
|
||||
mod rules;
|
||||
|
||||
pub(crate) use download::init as init_download_semaphore;
|
||||
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
|
||||
use d::minecraft::{Rule, RuleAction};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LauncherError {
|
||||
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||
ChecksumFailure {
|
||||
hash: String,
|
||||
url: String,
|
||||
tries: u32,
|
||||
},
|
||||
let res = match rule {
|
||||
Rule {
|
||||
os: Some(ref os), ..
|
||||
} => crate::util::platform::os_rule(os),
|
||||
Rule {
|
||||
features: Some(ref features),
|
||||
..
|
||||
} => features.has_demo_resolution.unwrap_or(false),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
#[error("Failed to run processor: {0}")]
|
||||
ProcessorError(String),
|
||||
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Error while managing asynchronous tasks")]
|
||||
TaskError(#[from] tokio::task::JoinError),
|
||||
|
||||
#[error("Error while reading/writing to the disk: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Error while spawning child process {process}")]
|
||||
ProcessError {
|
||||
inner: std::io::Error,
|
||||
process: String,
|
||||
},
|
||||
|
||||
#[error("Error while deserializing JSON")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Unable to fetch {item}")]
|
||||
FetchError { inner: reqwest::Error, item: String },
|
||||
|
||||
#[error("{0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Error while fetching metadata: {0}")]
|
||||
DaedalusError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Error while reading metadata: {0}")]
|
||||
MetaError(#[from] crate::data::DataError),
|
||||
|
||||
#[error("Java error: {0}")]
|
||||
JavaError(String),
|
||||
|
||||
#[error("Command exited with non-zero exit code: {0}")]
|
||||
ExitError(i32),
|
||||
}
|
||||
|
||||
// TODO: this probably should be in crate::data
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ModLoader {
|
||||
Vanilla,
|
||||
Forge,
|
||||
Fabric,
|
||||
}
|
||||
|
||||
impl Default for ModLoader {
|
||||
fn default() -> Self {
|
||||
ModLoader::Vanilla
|
||||
match rule.action {
|
||||
RuleAction::Allow => res,
|
||||
RuleAction::Disallow => !res,
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModLoader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let repr = match self {
|
||||
&Self::Vanilla => "Vanilla",
|
||||
&Self::Forge => "Forge",
|
||||
&Self::Fabric => "Fabric",
|
||||
};
|
||||
|
||||
f.write_str(repr)
|
||||
macro_rules! processor_rules {
|
||||
($dest:expr; $($name:literal : client => $client:expr, server => $server:expr;)+) => {
|
||||
$(std::collections::HashMap::insert(
|
||||
$dest,
|
||||
String::from($name),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: String::from($client),
|
||||
server: String::from($server),
|
||||
},
|
||||
);)+
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn launch_minecraft(
|
||||
game_version: &str,
|
||||
loader_version: &Option<LoaderVersion>,
|
||||
root_dir: &Path,
|
||||
java: &Path,
|
||||
java_args: &Vec<String>,
|
||||
loader_version: &Option<d::modded::LoaderVersion>,
|
||||
instance_path: &Path,
|
||||
java_install: &Path,
|
||||
java_args: &[String],
|
||||
wrapper: &Option<String>,
|
||||
memory: &crate::data::profiles::MemorySettings,
|
||||
resolution: &crate::data::profiles::WindowSize,
|
||||
credentials: &Credentials,
|
||||
) -> Result<Child, LauncherError> {
|
||||
let (metadata, settings) = futures::try_join! {
|
||||
crate::data::Metadata::get(),
|
||||
crate::data::Settings::get(),
|
||||
}?;
|
||||
let root_dir = root_dir.canonicalize()?;
|
||||
let metadata_dir = &settings.metadata_dir;
|
||||
memory: &st::MemorySettings,
|
||||
resolution: &st::WindowSize,
|
||||
credentials: &auth::Credentials,
|
||||
) -> crate::Result<Child> {
|
||||
let state = st::State::get().await?;
|
||||
let instance_path = instance_path.canonicalize()?;
|
||||
|
||||
let (
|
||||
versions_path,
|
||||
libraries_path,
|
||||
assets_path,
|
||||
legacy_assets_path,
|
||||
natives_path,
|
||||
) = (
|
||||
metadata_dir.join("versions"),
|
||||
metadata_dir.join("libraries"),
|
||||
metadata_dir.join("assets"),
|
||||
metadata_dir.join("resources"),
|
||||
metadata_dir.join("natives"),
|
||||
);
|
||||
|
||||
let version = metadata
|
||||
let version = state
|
||||
.metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == game_version)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Invalid game version: {game_version}",
|
||||
))
|
||||
})?;
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
"Invalid game version: {game_version}"
|
||||
)))?;
|
||||
|
||||
let version_jar = loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| it.id.clone());
|
||||
|
||||
let mut version = download::download_version_info(
|
||||
&versions_path,
|
||||
version,
|
||||
let mut version_info = download::download_version_info(
|
||||
&state,
|
||||
&version,
|
||||
loader_version.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_path = versions_path
|
||||
.join(&version.id)
|
||||
.join(format!("{}.jar", &version_jar));
|
||||
let version_natives_path = natives_path.join(&version.id);
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version.id)
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
download_minecraft(
|
||||
&version,
|
||||
&versions_path,
|
||||
&assets_path,
|
||||
&legacy_assets_path,
|
||||
&libraries_path,
|
||||
&version_natives_path,
|
||||
)
|
||||
.await?;
|
||||
download::download_minecraft(&state, &version_info).await?;
|
||||
st::State::sync().await?;
|
||||
|
||||
if let Some(processors) = &version.processors {
|
||||
if let Some(ref mut data) = version.data {
|
||||
data.insert(
|
||||
"SIDE".to_string(),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: "client".to_string(),
|
||||
server: "".to_string(),
|
||||
},
|
||||
);
|
||||
data.insert(
|
||||
"MINECRAFT_JAR".to_string(),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: client_path.to_string_lossy().to_string(),
|
||||
server: "".to_string(),
|
||||
},
|
||||
);
|
||||
data.insert(
|
||||
"MINECRAFT_VERSION".to_string(),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: game_version.to_string(),
|
||||
server: "".to_string(),
|
||||
},
|
||||
);
|
||||
data.insert(
|
||||
"ROOT".to_string(),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: root_dir.to_string_lossy().to_string(),
|
||||
server: "".to_string(),
|
||||
},
|
||||
);
|
||||
data.insert(
|
||||
"LIBRARY_DIR".to_string(),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: libraries_path.to_string_lossy().to_string(),
|
||||
server: "".to_string(),
|
||||
},
|
||||
);
|
||||
if let Some(processors) = &version_info.processors {
|
||||
if let Some(ref mut data) = version_info.data {
|
||||
processor_rules! {
|
||||
data;
|
||||
"SIDE":
|
||||
client => "client",
|
||||
server => "";
|
||||
"MINECRAFT_JAR" :
|
||||
client => client_path.to_string_lossy(),
|
||||
server => "";
|
||||
"MINECRAFT_VERSION":
|
||||
client => game_version,
|
||||
server => "";
|
||||
"ROOT":
|
||||
client => instance_path.to_string_lossy(),
|
||||
server => "";
|
||||
"LIBRARY_DIR":
|
||||
client => state.directories.libraries_dir().to_string_lossy(),
|
||||
server => "";
|
||||
}
|
||||
|
||||
for processor in processors {
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&"client".to_string()) {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -209,120 +120,93 @@ pub async fn launch_minecraft(
|
||||
|
||||
let child = Command::new("java")
|
||||
.arg("-cp")
|
||||
.arg(args::get_class_paths_jar(&libraries_path, &cp)?)
|
||||
.arg(args::get_class_paths_jar(
|
||||
&state.directories.libraries_dir(),
|
||||
&cp,
|
||||
)?)
|
||||
.arg(
|
||||
args::get_processor_main_class(args::get_lib_path(
|
||||
&libraries_path,
|
||||
&state.directories.libraries_dir(),
|
||||
&processor.jar,
|
||||
)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
LauncherError::ProcessorError(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Could not find processor main class for {}",
|
||||
processor.jar
|
||||
))
|
||||
})?,
|
||||
)
|
||||
.args(args::get_processor_arguments(
|
||||
&libraries_path,
|
||||
&state.directories.libraries_dir(),
|
||||
&processor.args,
|
||||
data,
|
||||
)?)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: "java".to_string(),
|
||||
.map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"Error running processor: {err}",
|
||||
))
|
||||
})?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(LauncherError::ProcessorError(
|
||||
String::from_utf8_lossy(&child.stderr).to_string(),
|
||||
));
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
"Processor error: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arguments = version.arguments.clone().unwrap_or_default();
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
let mut cmd = Command::new(hook);
|
||||
cmd.arg(java);
|
||||
cmd.arg(java_install);
|
||||
cmd
|
||||
}
|
||||
None => Command::new(java.to_string_lossy().to_string()),
|
||||
None => Command::new(String::from(java_install.to_string_lossy())),
|
||||
};
|
||||
|
||||
command
|
||||
.args(args::get_jvm_arguments(
|
||||
arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()),
|
||||
&version_natives_path,
|
||||
&libraries_path,
|
||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||
.map(|x| x.as_slice()),
|
||||
&state.directories.version_natives_dir(&version.id),
|
||||
&state.directories.libraries_dir(),
|
||||
&args::get_class_paths(
|
||||
&libraries_path,
|
||||
version.libraries.as_slice(),
|
||||
&state.directories.libraries_dir(),
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
java_args.clone(),
|
||||
Vec::from(java_args),
|
||||
)?)
|
||||
.arg(version.main_class.clone())
|
||||
.arg(version_info.main_class.clone())
|
||||
.args(args::get_minecraft_arguments(
|
||||
arguments.get(&ArgumentType::Game).map(|x| x.as_slice()),
|
||||
version.minecraft_arguments.as_deref(),
|
||||
args.get(&d::minecraft::ArgumentType::Game)
|
||||
.map(|x| x.as_slice()),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version.asset_index.id,
|
||||
&root_dir,
|
||||
&assets_path,
|
||||
&version_info.asset_index.id,
|
||||
&instance_path,
|
||||
&state.directories.assets_dir(),
|
||||
&version.type_,
|
||||
*resolution,
|
||||
)?)
|
||||
.current_dir(root_dir.clone())
|
||||
.current_dir(instance_path.clone())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
command.spawn().map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: format!("minecraft-{} @ {}", &version.id, root_dir.display()),
|
||||
command.spawn().map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"Error running Minecraft (minecraft-{} @ {}): {err}",
|
||||
&version.id,
|
||||
instance_path.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download_minecraft(
|
||||
version: &VersionInfo,
|
||||
versions_dir: &Path,
|
||||
assets_dir: &Path,
|
||||
legacy_assets_dir: &Path,
|
||||
libraries_dir: &Path,
|
||||
natives_dir: &Path,
|
||||
) -> Result<(), LauncherError> {
|
||||
let assets_index =
|
||||
download::download_assets_index(assets_dir, version).await?;
|
||||
|
||||
let (a, b, c) = futures::future::join3(
|
||||
download::download_client(versions_dir, version),
|
||||
download::download_assets(
|
||||
assets_dir,
|
||||
if version.assets == "legacy" {
|
||||
Some(legacy_assets_dir)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
&assets_index,
|
||||
),
|
||||
download::download_libraries(
|
||||
libraries_dir,
|
||||
natives_dir,
|
||||
version.libraries.as_slice(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
a?;
|
||||
b?;
|
||||
c?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::launcher::download::get_os;
|
||||
use daedalus::minecraft::{OsRule, Rule, RuleAction};
|
||||
use regex::Regex;
|
||||
|
||||
pub fn parse_rules(rules: &[Rule]) -> bool {
|
||||
rules.iter().all(|x| parse_rule(x))
|
||||
}
|
||||
|
||||
pub fn parse_rule(rule: &Rule) -> bool {
|
||||
let result = if let Some(os) = &rule.os {
|
||||
parse_os_rule(os)
|
||||
} else if let Some(features) = &rule.features {
|
||||
features.has_demo_resolution.unwrap_or(false)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
match rule.action {
|
||||
RuleAction::Allow => result,
|
||||
RuleAction::Disallow => !result,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_os_rule(rule: &OsRule) -> bool {
|
||||
if let Some(arch) = &rule.arch {
|
||||
match arch.as_str() {
|
||||
"x86" => {
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
return false;
|
||||
}
|
||||
"arm" => {
|
||||
#[cfg(not(target_arch = "arm"))]
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
if &get_os() != name {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(version) = &rule.version {
|
||||
let regex = Regex::new(version.as_str());
|
||||
|
||||
if let Ok(regex) = regex {
|
||||
if !regex.is_match(&sys_info::os_release().unwrap_or_default()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -1,57 +1,19 @@
|
||||
//! # Theseus
|
||||
//!
|
||||
//! Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
||||
//! and launching Modrinth mod packs
|
||||
/*!
|
||||
# Theseus
|
||||
|
||||
Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
||||
and launching Modrinth mod packs
|
||||
*/
|
||||
#![warn(unused_import_braces, missing_debug_implementations)]
|
||||
#![deny(unused_must_use)]
|
||||
|
||||
// TODO: make non-hardcoded
|
||||
lazy_static::lazy_static! {
|
||||
static ref LAUNCHER_WORK_DIR: std::path::PathBuf = dirs::config_dir().expect("Could not find config dir").join("theseus");
|
||||
}
|
||||
|
||||
pub mod data;
|
||||
pub mod launcher;
|
||||
pub mod modpack;
|
||||
mod api;
|
||||
mod config;
|
||||
mod error;
|
||||
mod launcher;
|
||||
mod state;
|
||||
mod util;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Launcher error: {0}")]
|
||||
LauncherError(#[from] launcher::LauncherError),
|
||||
|
||||
#[error("Modpack error: {0}")]
|
||||
ModpackError(#[from] modpack::ModpackError),
|
||||
|
||||
#[error("Data error: {0}")]
|
||||
DaedalusError(#[from] data::DataError),
|
||||
}
|
||||
|
||||
pub async fn init() -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(LAUNCHER_WORK_DIR.as_path())
|
||||
.await
|
||||
.expect("Unable to create launcher root directory!");
|
||||
|
||||
use crate::data::*;
|
||||
Metadata::init().await?;
|
||||
|
||||
Settings::init().await?;
|
||||
|
||||
tokio::try_join! {
|
||||
launcher::init_download_semaphore(),
|
||||
Profiles::init(),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save() -> Result<(), Error> {
|
||||
use crate::data::*;
|
||||
|
||||
tokio::try_join! {
|
||||
Settings::save(),
|
||||
Profiles::save(),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub use api::*;
|
||||
pub use error::*;
|
||||
pub use state::State;
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::launcher::ModLoader;
|
||||
|
||||
use super::pack::ModpackGame;
|
||||
use super::{pack, ModpackError, ModpackResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const DEFAULT_FORMAT_VERSION: u32 = 1;
|
||||
const MODRINTH_GAMEDATA_URL: &str = "https://staging-cdn.modrinth.com/gamedata";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Manifest {
|
||||
pub format_version: u32,
|
||||
pub game: String,
|
||||
pub version_id: String,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub files: Vec<ManifestFile>,
|
||||
pub dependencies: ManifestDeps,
|
||||
}
|
||||
|
||||
impl TryFrom<Manifest> for pack::Modpack {
|
||||
type Error = ModpackError;
|
||||
|
||||
fn try_from(manifest: Manifest) -> Result<Self, Self::Error> {
|
||||
let files = manifest
|
||||
.files
|
||||
.into_iter()
|
||||
.map(pack::ModpackFile::try_from)
|
||||
.collect::<ModpackResult<_>>()?;
|
||||
|
||||
Ok(Self {
|
||||
name: manifest.name,
|
||||
version: manifest.version_id,
|
||||
summary: manifest.summary,
|
||||
game: ModpackGame::from(manifest.dependencies),
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_loader_version(loader: ModLoader, version: &str) -> ModpackResult<String> {
|
||||
let source = match loader {
|
||||
ModLoader::Vanilla => Err(ModpackError::VersionError(String::from(
|
||||
"Attempted to get mod loader version of Vanilla",
|
||||
))),
|
||||
ModLoader::Forge => Ok(format!("{MODRINTH_GAMEDATA_URL}/forge/v0/manifest.json")),
|
||||
ModLoader::Fabric => Ok(format!("{MODRINTH_GAMEDATA_URL}/fabric/v0/manifest.json")),
|
||||
}?;
|
||||
let manifest = futures::executor::block_on(daedalus::modded::fetch_manifest(&source))?;
|
||||
|
||||
let version = manifest
|
||||
.game_versions
|
||||
.iter()
|
||||
.find(|&it| it.id == version)
|
||||
.map(|x| x.loaders.first())
|
||||
.flatten()
|
||||
.ok_or_else(|| {
|
||||
ModpackError::VersionError(format!(
|
||||
"No versions of modloader {loader:?} exist for Minecraft {version}",
|
||||
))
|
||||
})?;
|
||||
Ok(version.id.clone())
|
||||
}
|
||||
|
||||
impl TryFrom<pack::Modpack> for Manifest {
|
||||
type Error = ModpackError;
|
||||
|
||||
fn try_from(pack: pack::Modpack) -> Result<Self, Self::Error> {
|
||||
let pack::Modpack {
|
||||
game,
|
||||
version,
|
||||
name,
|
||||
summary,
|
||||
files,
|
||||
} = pack;
|
||||
|
||||
let game_name = match &game {
|
||||
ModpackGame::Minecraft(..) => "minecraft".into(),
|
||||
};
|
||||
|
||||
let files: Vec<_> = files.into_iter().map(ManifestFile::from).collect();
|
||||
|
||||
Ok(Manifest {
|
||||
format_version: DEFAULT_FORMAT_VERSION,
|
||||
game: game_name,
|
||||
version_id: version,
|
||||
name,
|
||||
summary,
|
||||
files,
|
||||
dependencies: ManifestDeps::try_from(game)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ManifestFile {
|
||||
pub path: PathBuf,
|
||||
pub hashes: Option<ManifestHashes>,
|
||||
#[serde(default)]
|
||||
pub env: ManifestEnvs,
|
||||
pub downloads: Vec<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<ManifestFile> for pack::ModpackFile {
|
||||
type Error = ModpackError;
|
||||
|
||||
fn try_from(file: ManifestFile) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
path: file.path,
|
||||
hashes: file.hashes.map(pack::ModpackFileHashes::from),
|
||||
env: pack::ModpackEnv::try_from(file.env)?,
|
||||
downloads: file.downloads.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pack::ModpackFile> for ManifestFile {
|
||||
fn from(file: pack::ModpackFile) -> Self {
|
||||
Self {
|
||||
path: file.path,
|
||||
hashes: file.hashes.map(ManifestHashes::from),
|
||||
env: file.env.into(),
|
||||
downloads: file.downloads.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ManifestHashes {
|
||||
pub sha1: String,
|
||||
}
|
||||
|
||||
impl From<ManifestHashes> for pack::ModpackFileHashes {
|
||||
fn from(hashes: ManifestHashes) -> Self {
|
||||
Self { sha1: hashes.sha1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pack::ModpackFileHashes> for ManifestHashes {
|
||||
fn from(hashes: pack::ModpackFileHashes) -> Self {
|
||||
Self { sha1: hashes.sha1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ManifestEnvs {
|
||||
pub client: ManifestEnv,
|
||||
pub server: ManifestEnv,
|
||||
}
|
||||
|
||||
impl Default for ManifestEnvs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
client: ManifestEnv::Optional,
|
||||
server: ManifestEnv::Optional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ManifestEnv {
|
||||
Required,
|
||||
Optional,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl TryFrom<ManifestEnvs> for pack::ModpackEnv {
|
||||
type Error = ModpackError;
|
||||
|
||||
fn try_from(envs: ManifestEnvs) -> Result<Self, Self::Error> {
|
||||
use ManifestEnv::*;
|
||||
|
||||
match (envs.client, envs.server) {
|
||||
(Required, Unsupported) => Ok(Self::ClientOnly),
|
||||
(Unsupported, Required) => Ok(Self::ServerOnly),
|
||||
(Optional, Optional) => Ok(Self::Both),
|
||||
_ => Err(ModpackError::FormatError(format!(
|
||||
"Invalid environment specification: {:?}",
|
||||
envs
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pack::ModpackEnv> for ManifestEnvs {
|
||||
fn from(envs: pack::ModpackEnv) -> Self {
|
||||
use super::pack::ModpackEnv::*;
|
||||
|
||||
let (client, server) = match envs {
|
||||
ClientOnly => (ManifestEnv::Required, ManifestEnv::Unsupported),
|
||||
ServerOnly => (ManifestEnv::Unsupported, ManifestEnv::Required),
|
||||
Both => (ManifestEnv::Optional, ManifestEnv::Optional),
|
||||
};
|
||||
|
||||
Self { client, server }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum ManifestDeps {
|
||||
MinecraftFabric {
|
||||
minecraft: String,
|
||||
#[serde(rename = "fabric-loader")]
|
||||
fabric_loader: String,
|
||||
},
|
||||
MinecraftForge {
|
||||
minecraft: String,
|
||||
forge: String,
|
||||
},
|
||||
MinecraftVanilla {
|
||||
minecraft: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ManifestDeps> for pack::ModpackGame {
|
||||
fn from(deps: ManifestDeps) -> Self {
|
||||
use ManifestDeps::*;
|
||||
|
||||
match deps {
|
||||
MinecraftVanilla { minecraft } => Self::Minecraft(minecraft, ModLoader::Vanilla),
|
||||
MinecraftFabric { minecraft, .. } => Self::Minecraft(minecraft, ModLoader::Fabric),
|
||||
MinecraftForge { minecraft, .. } => Self::Minecraft(minecraft, ModLoader::Forge),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pack::ModpackGame> for ManifestDeps {
|
||||
type Error = ModpackError;
|
||||
|
||||
fn try_from(game: pack::ModpackGame) -> Result<Self, Self::Error> {
|
||||
use super::pack::ModpackGame::*;
|
||||
Ok(match game {
|
||||
Minecraft(minecraft, ModLoader::Vanilla) => Self::MinecraftVanilla { minecraft },
|
||||
Minecraft(minecraft, ModLoader::Fabric) => Self::MinecraftFabric {
|
||||
fabric_loader: get_loader_version(ModLoader::Fabric, &minecraft)?,
|
||||
minecraft,
|
||||
},
|
||||
Minecraft(minecraft, ModLoader::Forge) => Self::MinecraftForge {
|
||||
forge: get_loader_version(ModLoader::Fabric, &minecraft)?,
|
||||
minecraft,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple() -> ModpackResult<()> {
|
||||
const PACK_JSON: &'static str = r#"
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"game": "minecraft",
|
||||
"versionId": "deadbeef",
|
||||
"name": "Example Pack",
|
||||
"files": [],
|
||||
"dependencies": {
|
||||
"minecraft": "1.17.1"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let expected_manifest = Manifest {
|
||||
format_version: 1,
|
||||
game: "minecraft".into(),
|
||||
version_id: "deadbeef".into(),
|
||||
name: "Example Pack".into(),
|
||||
summary: None,
|
||||
files: vec![],
|
||||
dependencies: ManifestDeps::MinecraftVanilla {
|
||||
minecraft: "1.17.1".into(),
|
||||
},
|
||||
};
|
||||
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
|
||||
|
||||
assert_eq!(expected_manifest, manifest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_forge() -> ModpackResult<()> {
|
||||
const PACK_JSON: &'static str = r#"
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"game": "minecraft",
|
||||
"versionId": "deadbeef",
|
||||
"name": "Example Pack",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/testmod.jar",
|
||||
"hashes": {
|
||||
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"downloads": [
|
||||
"https://example.com/testmod.jar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"minecraft": "1.17.1",
|
||||
"forge": "37.0.110"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let expected_manifest = Manifest {
|
||||
format_version: 1,
|
||||
game: "minecraft".into(),
|
||||
version_id: "deadbeef".into(),
|
||||
name: "Example Pack".into(),
|
||||
summary: None,
|
||||
files: vec![ManifestFile {
|
||||
path: "mods/testmod.jar".into(),
|
||||
hashes: Some(ManifestHashes {
|
||||
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
|
||||
}),
|
||||
env: ManifestEnvs::default(),
|
||||
downloads: vec!["https://example.com/testmod.jar".into()],
|
||||
}],
|
||||
dependencies: ManifestDeps::MinecraftForge {
|
||||
minecraft: "1.17.1".into(),
|
||||
forge: "37.0.110".into(),
|
||||
},
|
||||
};
|
||||
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
|
||||
|
||||
assert_eq!(expected_manifest, manifest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fabric() -> ModpackResult<()> {
|
||||
const PACK_JSON: &'static str = r#"
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"game": "minecraft",
|
||||
"versionId": "deadbeef",
|
||||
"name": "Example Pack",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/testmod.jar",
|
||||
"hashes": {
|
||||
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"downloads": [
|
||||
"https://example.com/testmod.jar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"minecraft": "1.17.1",
|
||||
"fabric-loader": "0.9.0"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let expected_manifest = Manifest {
|
||||
format_version: 1,
|
||||
game: "minecraft".into(),
|
||||
version_id: "deadbeef".into(),
|
||||
name: "Example Pack".into(),
|
||||
summary: None,
|
||||
files: vec![ManifestFile {
|
||||
path: "mods/testmod.jar".into(),
|
||||
hashes: Some(ManifestHashes {
|
||||
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
|
||||
}),
|
||||
env: ManifestEnvs::default(),
|
||||
downloads: vec!["https://example.com/testmod.jar".into()],
|
||||
}],
|
||||
dependencies: ManifestDeps::MinecraftFabric {
|
||||
minecraft: "1.17.1".into(),
|
||||
fabric_loader: "0.9.0".into(),
|
||||
},
|
||||
};
|
||||
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
|
||||
|
||||
assert_eq!(expected_manifest, manifest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_complete() -> ModpackResult<()> {
|
||||
const PACK_JSON: &'static str = r#"
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"game": "minecraft",
|
||||
"versionId": "deadbeef",
|
||||
"name": "Example Pack",
|
||||
"summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/testmod.jar",
|
||||
"hashes": {
|
||||
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "unsupported"
|
||||
},
|
||||
"downloads": [
|
||||
"https://example.com/testmod.jar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"minecraft": "1.17.1",
|
||||
"forge": "37.0.110"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let expected_manifest = Manifest {
|
||||
format_version: 1,
|
||||
game: "minecraft".into(),
|
||||
version_id: "deadbeef".into(),
|
||||
name: "Example Pack".into(),
|
||||
summary: Some("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.".into()),
|
||||
files: vec![ManifestFile {
|
||||
path: "mods/testmod.jar".into(),
|
||||
hashes: Some(ManifestHashes {
|
||||
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
|
||||
}),
|
||||
env: ManifestEnvs {
|
||||
client: ManifestEnv::Required,
|
||||
server: ManifestEnv::Unsupported,
|
||||
},
|
||||
downloads: vec!["https://example.com/testmod.jar".into()],
|
||||
}],
|
||||
dependencies: ManifestDeps::MinecraftForge {
|
||||
minecraft: "1.17.1".into(),
|
||||
forge: "37.0.110".into(),
|
||||
},
|
||||
};
|
||||
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
|
||||
|
||||
assert_eq!(expected_manifest, manifest);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
//! Provides utilties for downloading and parsing modpacks
|
||||
|
||||
use daedalus::download_file;
|
||||
use fs_extra::dir::CopyOptions;
|
||||
use std::{convert::TryFrom, env, io, path::Path};
|
||||
use tokio::{fs, try_join};
|
||||
use uuid::Uuid;
|
||||
use zip::ZipArchive;
|
||||
use zip_extensions::ZipWriterExtensions;
|
||||
|
||||
use self::{
|
||||
manifest::Manifest,
|
||||
pack::{Modpack, ModpackGame},
|
||||
};
|
||||
|
||||
pub mod manifest;
|
||||
pub mod modrinth_api;
|
||||
pub mod pack;
|
||||
|
||||
pub const COMPILED_PATH: &str = "compiled/";
|
||||
pub const COMPILED_ZIP: &str = "compiled.mrpack";
|
||||
pub const MANIFEST_PATH: &str = "modrinth.index.json";
|
||||
pub const OVERRIDES_PATH: &str = "overrides/";
|
||||
pub const PACK_JSON5_PATH: &str = "modpack.json5";
|
||||
const PACK_GITIGNORE: &'static str = const_format::formatcp!(
|
||||
r#"
|
||||
{COMPILED_PATH}
|
||||
{COMPILED_ZIP}
|
||||
"#
|
||||
);
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ModpackError {
|
||||
#[error("I/O error while reading modpack: {0}")]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error("I/O error while reading modpack: {0}")]
|
||||
FSExtraError(#[from] fs_extra::error::Error),
|
||||
|
||||
#[error("Error extracting archive: {0}")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("Invalid modpack format: {0}")]
|
||||
FormatError(String),
|
||||
|
||||
#[error("Invalid output directory: {0}")]
|
||||
InvalidDirectory(String),
|
||||
|
||||
#[error("Error parsing manifest: {0}")]
|
||||
ManifestError(String),
|
||||
|
||||
#[error("Daedalus error: {0}")]
|
||||
DaedalusError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Error parsing json: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Error parsing json5: {0}")]
|
||||
Json5Error(#[from] json5::Error),
|
||||
|
||||
#[error("Error joining futures: {0}")]
|
||||
JoinError(#[from] tokio::task::JoinError),
|
||||
|
||||
#[error("Versioning Error: {0}")]
|
||||
VersionError(String),
|
||||
|
||||
#[error("Error downloading file: {0}")]
|
||||
FetchError(#[from] reqwest::Error),
|
||||
|
||||
#[error("Invalid modpack source: {0} (set the WHITELISTED_MODPACK_DOMAINS environment variable to override)")]
|
||||
SourceWhitelistError(String),
|
||||
}
|
||||
|
||||
type ModpackResult<T> = Result<T, ModpackError>;
|
||||
|
||||
/// Realise a modpack from a given URL
|
||||
pub async fn fetch_modpack(
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
dest: &Path,
|
||||
side: pack::ModpackSide,
|
||||
) -> ModpackResult<()> {
|
||||
let bytes = download_file(url, sha1).await?;
|
||||
let mut archive = ZipArchive::new(io::Cursor::new(&bytes as &[u8]))?;
|
||||
realise_modpack_zip(&mut archive, dest, side).await
|
||||
}
|
||||
|
||||
/// Realise a given modpack from a zip archive
|
||||
pub async fn realise_modpack_zip(
|
||||
archive: &mut ZipArchive<impl io::Read + io::Seek>,
|
||||
dest: &Path,
|
||||
side: pack::ModpackSide,
|
||||
) -> ModpackResult<()> {
|
||||
let mut tmp = env::temp_dir();
|
||||
tmp.push(format!("theseus-{}/", Uuid::new_v4()));
|
||||
archive.extract(&tmp)?;
|
||||
realise_modpack(&tmp, dest, side).await
|
||||
}
|
||||
|
||||
/// Realise a given modpack into an instance
|
||||
pub async fn realise_modpack(
|
||||
dir: &Path,
|
||||
dest: &Path,
|
||||
side: pack::ModpackSide,
|
||||
) -> ModpackResult<()> {
|
||||
if dest.is_file() {
|
||||
return Err(ModpackError::InvalidDirectory(String::from(
|
||||
"Output is not a directory",
|
||||
)));
|
||||
}
|
||||
if std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) {
|
||||
return Err(ModpackError::InvalidDirectory(String::from(
|
||||
"Output directory is non-empty",
|
||||
)));
|
||||
}
|
||||
if !dest.exists() {
|
||||
fs::create_dir_all(dest).await?;
|
||||
}
|
||||
|
||||
// Copy overrides
|
||||
let overrides = dir.join(OVERRIDES_PATH);
|
||||
if overrides.is_dir() {
|
||||
fs_extra::dir::copy(overrides, dest, &CopyOptions::new())?;
|
||||
}
|
||||
|
||||
// Parse manifest
|
||||
// NOTE: I'm using standard files here, since Serde does not support async readers
|
||||
let manifest_path = Some(dir.join(MANIFEST_PATH))
|
||||
.filter(|it| it.is_file())
|
||||
.ok_or_else(|| {
|
||||
ModpackError::ManifestError(String::from("Manifest missing or is not a file"))
|
||||
})?;
|
||||
let manifest_file = std::fs::File::open(manifest_path)?;
|
||||
let reader = io::BufReader::new(manifest_file);
|
||||
|
||||
let manifest: Manifest = serde_json::from_reader(reader)?;
|
||||
let modpack = Modpack::try_from(manifest)?;
|
||||
|
||||
// Realise modpack
|
||||
modpack.download_files(dest, side).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_pack_json5(pack: &Modpack) -> ModpackResult<String> {
|
||||
let json5 = json5::to_string(pack)?;
|
||||
Ok(format!("// This modpack is managed using Theseus. It can be edited using either a Theseus-compatible launcher or manually.\n{json5}"))
|
||||
}
|
||||
|
||||
pub async fn create_modpack(
|
||||
name: &str,
|
||||
game: ModpackGame,
|
||||
summary: Option<&str>,
|
||||
) -> ModpackResult<()> {
|
||||
let output_dir = Path::new("./").join(name);
|
||||
let pack = Modpack::new(game, "0.1.0", name, summary);
|
||||
|
||||
try_join!(
|
||||
fs::create_dir(&output_dir),
|
||||
fs::create_dir(output_dir.join(OVERRIDES_PATH)),
|
||||
fs::write(output_dir.join(".gitignore"), PACK_GITIGNORE),
|
||||
fs::write(output_dir.join(PACK_JSON5_PATH), to_pack_json5(&pack)?),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn compile_modpack(dir: &Path) -> ModpackResult<()> {
|
||||
let result_dir = dir.join(COMPILED_PATH);
|
||||
let pack: Modpack = json5::from_str(&fs::read_to_string(dir.join(PACK_JSON5_PATH)).await?)?;
|
||||
|
||||
fs::create_dir(&result_dir).await?;
|
||||
if dir.join(OVERRIDES_PATH).exists() {
|
||||
fs_extra::dir::copy(
|
||||
dir.join(OVERRIDES_PATH),
|
||||
result_dir.join(OVERRIDES_PATH),
|
||||
&CopyOptions::new(),
|
||||
)?;
|
||||
}
|
||||
let manifest = Manifest::try_from(pack)?;
|
||||
fs::write(
|
||||
result_dir.join(MANIFEST_PATH),
|
||||
serde_json::to_string(&manifest)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let result_zip = fs::File::create(dir.join(COMPILED_ZIP))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
let mut zip = zip::ZipWriter::new(&result_zip);
|
||||
zip.create_from_directory(&result_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
use std::{collections::HashSet, convert::TryFrom, path::PathBuf};
|
||||
|
||||
use crate::launcher::ModLoader;
|
||||
|
||||
use super::{
|
||||
manifest::{ManifestEnvs, ManifestHashes},
|
||||
pack::{ModpackEnv, ModpackFile, ModpackFileHashes, ModpackGame},
|
||||
ModpackError, ModpackResult,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::future::try_join_all;
|
||||
use serde::Deserialize;
|
||||
use tokio::try_join;
|
||||
|
||||
#[async_trait]
|
||||
pub trait ModrinthAPI {
|
||||
async fn get_latest_version(
|
||||
&self,
|
||||
project: &str,
|
||||
channel: &str,
|
||||
game: &ModpackGame,
|
||||
) -> ModpackResult<HashSet<ModpackFile>>;
|
||||
async fn get_version(
|
||||
&self,
|
||||
version: &str,
|
||||
) -> ModpackResult<HashSet<ModpackFile>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModrinthV1(pub String);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ModrinthV1Project {
|
||||
title: String,
|
||||
client_side: String,
|
||||
server_side: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ModrinthV1ProjectVersion {
|
||||
dependencies: HashSet<String>,
|
||||
game_versions: HashSet<String>,
|
||||
version_type: String,
|
||||
files: Vec<ModrinthV1ProjectVersionFile>,
|
||||
loaders: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct ModrinthV1ProjectVersionFile {
|
||||
hashes: ManifestHashes,
|
||||
url: String,
|
||||
filename: String,
|
||||
}
|
||||
|
||||
impl From<ModrinthV1ProjectVersionFile> for ModpackFile {
|
||||
fn from(file: ModrinthV1ProjectVersionFile) -> Self {
|
||||
Self {
|
||||
hashes: Some(ModpackFileHashes::from(file.hashes)),
|
||||
downloads: HashSet::from([file.url]),
|
||||
path: PathBuf::from(file.filename),
|
||||
// WARNING: Since the sidedness of version 1 API requests is unknown, the environment is
|
||||
// set here as both.
|
||||
env: ModpackEnv::Both,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ModrinthAPI for ModrinthV1 {
|
||||
async fn get_latest_version(
|
||||
&self,
|
||||
project: &str,
|
||||
channel: &str,
|
||||
game: &ModpackGame,
|
||||
) -> ModpackResult<HashSet<ModpackFile>> {
|
||||
let domain = &self.0;
|
||||
// Fetch metadata
|
||||
let (project_json, versions_json): (Bytes, Bytes) = try_join!(
|
||||
try_get_json(format!("{domain}/api/v1/mod/{project}")),
|
||||
try_get_json(format!("{domain}/api/v1/mod/{project}/version")),
|
||||
)?;
|
||||
|
||||
let (mut project_deserializer, mut versions_deserializer) = (
|
||||
serde_json::Deserializer::from_slice(&project_json),
|
||||
serde_json::Deserializer::from_slice(&versions_json),
|
||||
);
|
||||
|
||||
let (project, versions) = (
|
||||
ModrinthV1Project::deserialize(&mut project_deserializer)?,
|
||||
Vec::deserialize(&mut versions_deserializer)?,
|
||||
);
|
||||
|
||||
let (game_version, loader) = match game {
|
||||
ModpackGame::Minecraft(_, ModLoader::Vanilla) => Err(ModpackError::VersionError(
|
||||
String::from("Modrinth V1 does not support vanilla projects"),
|
||||
)),
|
||||
ModpackGame::Minecraft(ref version, ref loader) => Ok((version, loader)),
|
||||
// This guard is here for when Modrinth does support other games.
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(ModpackError::VersionError(String::from(
|
||||
"Attempted to use Modrinth API V1 to install a non-Minecraft project!",
|
||||
))),
|
||||
}?;
|
||||
|
||||
let version: ModrinthV1ProjectVersion = versions
|
||||
.into_iter()
|
||||
.find(|it: &ModrinthV1ProjectVersion| {
|
||||
let loader_str = match loader {
|
||||
ModLoader::Fabric => "fabric",
|
||||
ModLoader::Forge => "forge",
|
||||
ModLoader::Vanilla => unreachable!(),
|
||||
};
|
||||
it.version_type == channel
|
||||
&& it.game_versions.contains(game_version)
|
||||
&& it.loaders.contains(loader_str)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
ModpackError::VersionError(format!(
|
||||
"Unable to find compatible version of mod {}",
|
||||
project.title
|
||||
))
|
||||
})?;
|
||||
|
||||
// Project fields
|
||||
let envs = ModpackEnv::try_from(ManifestEnvs {
|
||||
client: serde_json::from_str(&project.client_side)?,
|
||||
server: serde_json::from_str(&project.server_side)?,
|
||||
})?;
|
||||
|
||||
// Conversions
|
||||
let files = version
|
||||
.files
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(ModpackFile::from)
|
||||
.collect::<HashSet<ModpackFile>>();
|
||||
|
||||
let dep_futures =
|
||||
version.dependencies.iter().map(|it| self.get_version(it));
|
||||
let deps = try_join_all(dep_futures)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<HashSet<ModpackFile>>();
|
||||
|
||||
Ok(files
|
||||
.into_iter()
|
||||
.chain(deps.into_iter())
|
||||
.map(|mut it| {
|
||||
it.env = envs;
|
||||
it
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_version(
|
||||
&self,
|
||||
version: &str,
|
||||
) -> ModpackResult<HashSet<ModpackFile>> {
|
||||
let domain = &self.0;
|
||||
let version_json =
|
||||
try_get_json(format!("{domain}/api/v1/version/{version}")).await?;
|
||||
let mut version_deserializer =
|
||||
serde_json::Deserializer::from_slice(&version_json);
|
||||
let version =
|
||||
ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
|
||||
|
||||
Ok(version
|
||||
.files
|
||||
.into_iter()
|
||||
.map(ModpackFile::from)
|
||||
.collect::<HashSet<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
async fn try_get_json(url: String) -> ModpackResult<Bytes> {
|
||||
Ok(reqwest::get(url).await?.error_for_status()?.bytes().await?)
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
use daedalus::download_file_mirrors;
|
||||
use futures::future;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::Hash,
|
||||
iter::FromIterator,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
use super::{
|
||||
modrinth_api::{self, ModrinthV1},
|
||||
ModpackError, ModpackResult,
|
||||
};
|
||||
use crate::launcher::ModLoader;
|
||||
|
||||
pub const MODRINTH_DEFAULT_MODPACK_DOMAINS: &[&str] = &[
|
||||
"cdn.modrinth.com",
|
||||
"edge.forgecdn.net",
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
];
|
||||
pub const MODRINTH_MODPACK_DOMAIN_WHITELIST_VAR: &str = "WHITELISTED_MODPACK_DOMAINS";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct Modpack {
|
||||
pub game: ModpackGame,
|
||||
pub version: String,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub files: HashSet<ModpackFile>,
|
||||
}
|
||||
|
||||
impl Modpack {
|
||||
pub fn new(game: ModpackGame, version: &str, name: &str, summary: Option<&str>) -> Self {
|
||||
Self {
|
||||
game,
|
||||
version: String::from(version),
|
||||
name: String::from(name),
|
||||
summary: summary.map(String::from),
|
||||
files: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Download a modpack's files for a given side to a given destination
|
||||
/// Assumes the destination exists and is a directory
|
||||
pub async fn download_files(&self, dest: &Path, side: ModpackSide) -> ModpackResult<()> {
|
||||
let handles = self.files.iter().cloned().map(move |file| {
|
||||
let (dest, side) = (dest.to_owned(), side);
|
||||
tokio::spawn(async move { file.fetch(&dest, side).await })
|
||||
});
|
||||
future::try_join_all(handles)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<ModpackResult<_>>()?;
|
||||
|
||||
// TODO Integrate instance format to save other metadata
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_project(
|
||||
&mut self,
|
||||
project: &str,
|
||||
base_path: &Path,
|
||||
source: Option<&dyn modrinth_api::ModrinthAPI>,
|
||||
channel: Option<&str>,
|
||||
) -> ModpackResult<()> {
|
||||
let default_api = ModrinthV1(String::from("https://api.modrinth.com"));
|
||||
let channel = channel.unwrap_or("release");
|
||||
let source = source.unwrap_or(&default_api);
|
||||
|
||||
let files = source
|
||||
.get_latest_version(project, channel, &self.game)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut it: ModpackFile| {
|
||||
it.path = base_path.join(it.path);
|
||||
it
|
||||
});
|
||||
|
||||
self.files.extend(files);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_version(
|
||||
&mut self,
|
||||
version: &str,
|
||||
base_path: &Path,
|
||||
source: Option<&dyn modrinth_api::ModrinthAPI>,
|
||||
) -> ModpackResult<()> {
|
||||
let default_api = ModrinthV1(String::from("https://api.modrinth.com"));
|
||||
let source = source.unwrap_or(&default_api);
|
||||
|
||||
let files = source
|
||||
.get_version(version)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut it: ModpackFile| {
|
||||
it.path = base_path.join(it.path);
|
||||
it
|
||||
});
|
||||
|
||||
self.files.extend(files);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_file(
|
||||
&mut self,
|
||||
source: reqwest::Url,
|
||||
dest: &Path,
|
||||
hashes: Option<ModpackFileHashes>,
|
||||
env: Option<ModpackEnv>,
|
||||
) -> ModpackResult<()> {
|
||||
let whitelisted_domains = std::env::var(MODRINTH_MODPACK_DOMAIN_WHITELIST_VAR)
|
||||
.map(|it| serde_json::from_str::<Vec<String>>(&it).ok().unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
MODRINTH_DEFAULT_MODPACK_DOMAINS
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(String::from)
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
|
||||
if !whitelisted_domains
|
||||
.iter()
|
||||
.any(|it| it == source.host_str().unwrap())
|
||||
{
|
||||
return Err(ModpackError::SourceWhitelistError(String::from(
|
||||
source.host_str().unwrap(),
|
||||
)));
|
||||
}
|
||||
|
||||
let file = ModpackFile {
|
||||
path: PathBuf::from(dest),
|
||||
hashes,
|
||||
env: env.unwrap_or(ModpackEnv::Both),
|
||||
downloads: HashSet::from_iter([String::from(source)].iter().cloned()),
|
||||
};
|
||||
|
||||
self.files.insert(file);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ModpackGame {
|
||||
// TODO: Currently, the launcher does not support specifying mod loader versions, so I just
|
||||
// store the loader here.
|
||||
Minecraft(String, ModLoader),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct ModpackFile {
|
||||
pub path: PathBuf,
|
||||
pub hashes: Option<ModpackFileHashes>,
|
||||
pub env: ModpackEnv,
|
||||
pub downloads: HashSet<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
impl Hash for ModpackFile {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
if let Some(ref hashes) = self.hashes {
|
||||
hashes.sha1.hash(state);
|
||||
}
|
||||
self.path.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl ModpackFile {
|
||||
pub async fn fetch(&self, dest: &Path, side: ModpackSide) -> ModpackResult<()> {
|
||||
if !self.env.supports(side) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = dest.join(&self.path);
|
||||
|
||||
// HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full
|
||||
// URLs, I'm supplying it with an empty string to avoid reinventing the wheel.
|
||||
let bytes = download_file_mirrors(
|
||||
"",
|
||||
self.downloads
|
||||
.iter()
|
||||
.map(|it| it.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice(),
|
||||
self.hashes.as_ref().map(|it| it.sha1.as_str()),
|
||||
)
|
||||
.await?;
|
||||
fs::create_dir_all(output.parent().unwrap()).await?;
|
||||
fs::write(output, bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModpackEnv {
|
||||
ClientOnly,
|
||||
ServerOnly,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModpackSide {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
impl ModpackEnv {
|
||||
pub fn supports(&self, side: ModpackSide) -> bool {
|
||||
match self {
|
||||
Self::ClientOnly => side == ModpackSide::Client,
|
||||
Self::ServerOnly => side == ModpackSide::Server,
|
||||
Self::Both => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct ModpackFileHashes {
|
||||
pub sha1: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::launcher::ModLoader;
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_version() -> ModpackResult<()> {
|
||||
const TEST_VERSION: &'static str = "TpnSObJ7";
|
||||
let mut test_pack = Modpack::new(
|
||||
ModpackGame::Minecraft(String::from("1.16.5"), ModLoader::Fabric),
|
||||
"0.1.0",
|
||||
"Example Modpack",
|
||||
None,
|
||||
);
|
||||
test_pack
|
||||
.add_version(TEST_VERSION, Path::new("mods/"), None)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
test_pack,
|
||||
Modpack {
|
||||
game: ModpackGame::Minecraft(String::from("1.16.5"), ModLoader::Fabric),
|
||||
version: String::from("0.1.0"),
|
||||
name: String::from("Example Modpack"),
|
||||
summary: None,
|
||||
files: {
|
||||
let mut files = HashSet::new();
|
||||
files.insert(ModpackFile {
|
||||
path: PathBuf::from("mods/gravestones-v1.9.jar"),
|
||||
hashes: Some(ModpackFileHashes {
|
||||
sha1: String::from("3f0f6d523d218460310b345be03ab3f1d294e04d"),
|
||||
}),
|
||||
env: ModpackEnv::Both,
|
||||
downloads: {
|
||||
let mut downloads = HashSet::new();
|
||||
downloads.insert(String::from("https://cdn.modrinth.com/data/ssUbhMkL/versions/v1.9/gravestones-v1.9.jar"));
|
||||
downloads
|
||||
}
|
||||
});
|
||||
files
|
||||
},
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
129
theseus/src/state/dirs.rs
Normal file
129
theseus/src/state/dirs.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
//! Theseus directory information
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryInfo {
|
||||
pub config_dir: PathBuf,
|
||||
pub working_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DirectoryInfo {
|
||||
/// Get all paths needed for Theseus to operate properly
|
||||
pub async fn init() -> crate::Result<Self> {
|
||||
// Working directory
|
||||
let working_dir = std::env::current_dir().map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Could not open working directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Config directory
|
||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
|
||||
.ok_or(crate::Error::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
))?;
|
||||
|
||||
fs::create_dir_all(&config_dir).await.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Error creating Theseus config directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
config_dir,
|
||||
working_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Minecraft instance metadata directory
|
||||
#[inline]
|
||||
pub fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("meta")
|
||||
}
|
||||
|
||||
/// Get the Minecraft versions metadata directory
|
||||
#[inline]
|
||||
pub fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("versions")
|
||||
}
|
||||
|
||||
/// Get the metadata directory for a given version
|
||||
#[inline]
|
||||
pub fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().join(version)
|
||||
}
|
||||
|
||||
/// Get the Minecraft libraries metadata directory
|
||||
#[inline]
|
||||
pub fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("libraries")
|
||||
}
|
||||
|
||||
/// Get the Minecraft assets metadata directory
|
||||
#[inline]
|
||||
pub fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("assets")
|
||||
}
|
||||
|
||||
/// Get the assets index directory
|
||||
#[inline]
|
||||
pub fn assets_index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("indexes")
|
||||
}
|
||||
|
||||
/// Get the assets objects directory
|
||||
#[inline]
|
||||
pub fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("objects")
|
||||
}
|
||||
|
||||
/// Get the directory for a specific object
|
||||
#[inline]
|
||||
pub fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().join(&hash[..2]).join(hash)
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("resources")
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("natives")
|
||||
}
|
||||
|
||||
/// Get the natives directory for a version of Minecraft
|
||||
#[inline]
|
||||
pub fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().join(version)
|
||||
}
|
||||
|
||||
/// Get the directory containing instance icons
|
||||
#[inline]
|
||||
pub fn icon_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("icons")
|
||||
}
|
||||
|
||||
/// Get the file containing the global database
|
||||
#[inline]
|
||||
pub fn database_file(&self) -> PathBuf {
|
||||
self.config_dir.join("data.bin")
|
||||
}
|
||||
|
||||
/// Get the settings file for Theseus
|
||||
#[inline]
|
||||
pub fn settings_file(&self) -> PathBuf {
|
||||
self.config_dir.join("settings.json")
|
||||
}
|
||||
|
||||
/// Get path from environment variable
|
||||
#[inline]
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
}
|
||||
90
theseus/src/state/metadata.rs
Normal file
90
theseus/src/state/metadata.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Theseus metadata
|
||||
use crate::config::BINCODE_CONFIG;
|
||||
use bincode::{Decode, Encode};
|
||||
use daedalus::{
|
||||
minecraft::{fetch_version_manifest, VersionManifest as MinecraftManifest},
|
||||
modded::{
|
||||
fetch_manifest as fetch_loader_manifest, Manifest as LoaderManifest,
|
||||
},
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::collections::LinkedList;
|
||||
|
||||
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
||||
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct Metadata {
|
||||
pub minecraft: MinecraftManifest,
|
||||
pub forge: LoaderManifest,
|
||||
pub fabric: LoaderManifest,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
fn get_manifest(name: &str) -> String {
|
||||
format!("{METADATA_URL}/{name}/v0/manifest.json")
|
||||
}
|
||||
|
||||
async fn fetch() -> crate::Result<Self> {
|
||||
let (minecraft, forge, fabric) = tokio::try_join! {
|
||||
async {
|
||||
let url = Self::get_manifest("minecraft");
|
||||
fetch_version_manifest(Some(&url)).await
|
||||
},
|
||||
async {
|
||||
let url = Self::get_manifest("forge");
|
||||
fetch_loader_manifest(&url).await
|
||||
},
|
||||
async {
|
||||
let url = Self::get_manifest("fabric");
|
||||
fetch_loader_manifest(&url).await
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(Self {
|
||||
minecraft,
|
||||
forge,
|
||||
fabric,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
|
||||
if let Some(ref meta_bin) = db.get(METADATA_DB_FIELD)? {
|
||||
match bincode::decode_from_slice::<Self, _>(
|
||||
&meta_bin,
|
||||
*BINCODE_CONFIG,
|
||||
) {
|
||||
Ok((meta, _)) => metadata = Some(meta),
|
||||
Err(err) => {
|
||||
log::warn!("Could not read launcher metadata: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut fetch_futures = LinkedList::new();
|
||||
for _ in 0..3 {
|
||||
fetch_futures.push_back(Self::fetch().boxed());
|
||||
}
|
||||
|
||||
match future::select_ok(fetch_futures).await {
|
||||
Ok(meta) => metadata = Some(meta.0),
|
||||
Err(err) => log::warn!("Unable to fetch launcher metadata: {err}"),
|
||||
}
|
||||
|
||||
if let Some(meta) = metadata {
|
||||
db.insert(
|
||||
METADATA_DB_FIELD,
|
||||
sled::IVec::from(bincode::encode_to_vec(
|
||||
&meta,
|
||||
*BINCODE_CONFIG,
|
||||
)?),
|
||||
)?;
|
||||
db.flush_async().await?;
|
||||
Ok(meta)
|
||||
} else {
|
||||
Err(crate::Error::NoValueFor(String::from("launcher metadata")))
|
||||
}
|
||||
}
|
||||
}
|
||||
118
theseus/src/state/mod.rs
Normal file
118
theseus/src/state/mod.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
//! Theseus state management system
|
||||
use crate::config::sled_config;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, OnceCell, RwLock, Semaphore};
|
||||
|
||||
// Submodules
|
||||
mod dirs;
|
||||
pub use self::dirs::*;
|
||||
|
||||
mod metadata;
|
||||
pub use metadata::*;
|
||||
|
||||
mod settings;
|
||||
pub use settings::*;
|
||||
|
||||
mod profiles;
|
||||
pub use profiles::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
/// Database, used to store some information
|
||||
pub(self) database: sled::Db,
|
||||
/// Information on the location of files used in the launcher
|
||||
pub directories: DirectoryInfo,
|
||||
/// Semaphore used to limit concurrent I/O and avoid errors
|
||||
pub io_semaphore: Semaphore,
|
||||
/// Launcher metadata
|
||||
pub metadata: Metadata,
|
||||
/// Launcher configuration
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Launcher profile metadata
|
||||
pub profiles: RwLock<Profiles>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(|| async {
|
||||
// Directories
|
||||
let directories = DirectoryInfo::init().await?;
|
||||
|
||||
// Database
|
||||
// TODO: make database versioned
|
||||
let database =
|
||||
sled_config().path(directories.database_file()).open()?;
|
||||
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
|
||||
// Metadata
|
||||
let metadata = Metadata::init(&database).await?;
|
||||
|
||||
// Profiles
|
||||
let profiles = Profiles::init(&database).await?;
|
||||
|
||||
// Loose initializations
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
io_semaphore,
|
||||
metadata,
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
/// Synchronize in-memory state with persistent state
|
||||
pub async fn sync() -> crate::Result<()> {
|
||||
let state = Self::get().await?;
|
||||
let batch = Arc::new(Mutex::new(sled::Batch::default()));
|
||||
|
||||
let sync_settings = async {
|
||||
let state = Arc::clone(&state);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let reader = state.settings.read().await;
|
||||
reader.sync(&state.directories.settings_file()).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let sync_profiles = async {
|
||||
let state = Arc::clone(&state);
|
||||
let batch = Arc::clone(&batch);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let profiles = state.profiles.read().await;
|
||||
let mut batch = batch.lock().await;
|
||||
|
||||
profiles.sync(&mut batch).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
tokio::try_join!(sync_settings, sync_profiles)?;
|
||||
|
||||
state
|
||||
.database
|
||||
.apply_batch(Arc::try_unwrap(batch).unwrap().into_inner())?;
|
||||
state.database.flush_async().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
340
theseus/src/state/profiles.rs
Normal file
340
theseus/src/state/profiles.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
use super::settings::{Hooks, MemorySettings, WindowSize};
|
||||
use crate::config::BINCODE_CONFIG;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use futures::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||
|
||||
// TODO: possibly add defaults to some of these values
|
||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
pub const SUPPORTED_ICON_FORMATS: &[&'static str] = &[
|
||||
"bmp", "gif", "jpeg", "jpg", "jpe", "png", "svg", "svgz", "webp", "rgb",
|
||||
"mp4",
|
||||
];
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub metadata: ProfileMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<MemorySettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<WindowSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hooks: Option<Hooks>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ProfileMetadata {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<PathBuf>,
|
||||
pub game_version: String,
|
||||
#[serde(default)]
|
||||
pub loader: ModLoader,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loader_version: Option<LoaderVersion>,
|
||||
pub format_version: u32,
|
||||
}
|
||||
|
||||
// TODO: Quilt?
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ModLoader {
|
||||
Vanilla,
|
||||
Forge,
|
||||
Fabric,
|
||||
}
|
||||
|
||||
impl Default for ModLoader {
|
||||
fn default() -> Self {
|
||||
ModLoader::Vanilla
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModLoader {
|
||||
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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct JavaSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub install: Option<PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_arguments: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub async fn new(
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(crate::Error::InputError(String::from(
|
||||
"Empty name for instance!",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path: path.canonicalize()?,
|
||||
metadata: ProfileMetadata {
|
||||
name,
|
||||
icon: None,
|
||||
game_version: version,
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: None,
|
||||
memory: None,
|
||||
resolution: None,
|
||||
hooks: None,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: deduplicate these builder methods
|
||||
// They are flat like this in order to allow builder-style usage
|
||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||
self.metadata.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn with_icon<'a>(
|
||||
&'a mut self,
|
||||
icon: &'a Path,
|
||||
) -> crate::Result<&'a mut Self> {
|
||||
let ext = icon
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("");
|
||||
|
||||
if SUPPORTED_ICON_FORMATS.contains(&ext) {
|
||||
let file_name = format!("icon.{ext}");
|
||||
fs::copy(icon, &self.path.join(&file_name)).await?;
|
||||
self.metadata.icon =
|
||||
Some(Path::new(&format!("./{file_name}")).to_owned());
|
||||
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(crate::Error::InputError(format!(
|
||||
"Unsupported image type: {ext}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||
self.metadata.game_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_loader(
|
||||
&mut self,
|
||||
loader: ModLoader,
|
||||
version: Option<LoaderVersion>,
|
||||
) -> &mut Self {
|
||||
self.metadata.loader = loader;
|
||||
self.metadata.loader_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_java_settings(
|
||||
&mut self,
|
||||
settings: Option<JavaSettings>,
|
||||
) -> &mut Self {
|
||||
self.java = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_memory(
|
||||
&mut self,
|
||||
settings: Option<MemorySettings>,
|
||||
) -> &mut Self {
|
||||
self.memory = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_resolution(
|
||||
&mut self,
|
||||
resolution: Option<WindowSize>,
|
||||
) -> &mut Self {
|
||||
self.resolution = resolution;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
||||
self.hooks = hooks;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
||||
Ok(Default::default()),
|
||||
|bytes| {
|
||||
bincode::decode_from_slice::<Box<[PathBuf]>, _>(
|
||||
&bytes,
|
||||
*BINCODE_CONFIG,
|
||||
)
|
||||
.map(|it| it.0)
|
||||
},
|
||||
)?;
|
||||
|
||||
let profiles = stream::iter(profile_db.iter())
|
||||
.then(|it| async move {
|
||||
let path = PathBuf::from(it);
|
||||
let prof = match Self::read_profile_from_dir(&path).await {
|
||||
Ok(prof) => Some(prof),
|
||||
Err(err) => {
|
||||
log::warn!("Error loading profile: {err}. Skipping...");
|
||||
None
|
||||
}
|
||||
};
|
||||
(path, prof)
|
||||
})
|
||||
.collect::<HashMap<PathBuf, Option<Profile>>>()
|
||||
.await;
|
||||
|
||||
Ok(Self(profiles))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
self.0.insert(
|
||||
profile
|
||||
.path
|
||||
.canonicalize()?
|
||||
.to_str()
|
||||
.ok_or(crate::Error::UTFError(profile.path.clone()))?
|
||||
.into(),
|
||||
Some(profile),
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn insert_from<'a>(
|
||||
&'a mut self,
|
||||
path: &'a Path,
|
||||
) -> crate::Result<&Self> {
|
||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
||||
self.0.remove(&path);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn sync<'a>(
|
||||
&'a self,
|
||||
batch: &'a mut sled::Batch,
|
||||
) -> crate::Result<&Self> {
|
||||
stream::iter(self.0.iter())
|
||||
.map(Ok::<_, crate::Error>)
|
||||
.try_for_each_concurrent(None, |(path, profile)| async move {
|
||||
let json = serde_json::to_vec_pretty(&profile)?;
|
||||
|
||||
let json_path =
|
||||
Path::new(path.to_str().unwrap()).join(PROFILE_JSON_PATH);
|
||||
|
||||
fs::write(json_path, json).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
batch.insert(
|
||||
PROFILE_SUBTREE,
|
||||
bincode::encode_to_vec(
|
||||
self.0.keys().collect::<Box<[_]>>(),
|
||||
*BINCODE_CONFIG,
|
||||
)?,
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> {
|
||||
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = PathBuf::from(path);
|
||||
Ok(profile)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn profile_test() -> Result<(), serde_json::Error> {
|
||||
let profile = Profile {
|
||||
path: PathBuf::new(),
|
||||
metadata: ProfileMetadata {
|
||||
name: String::from("Example Pack"),
|
||||
icon: None,
|
||||
game_version: String::from("1.18.2"),
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: Some(JavaSettings {
|
||||
install: Some(PathBuf::from("/usr/bin/java")),
|
||||
extra_arguments: Some(Vec::new()),
|
||||
}),
|
||||
memory: Some(MemorySettings {
|
||||
minimum: None,
|
||||
maximum: 8192,
|
||||
}),
|
||||
resolution: Some(WindowSize(1920, 1080)),
|
||||
hooks: Some(Hooks {
|
||||
pre_launch: HashSet::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::new(),
|
||||
}),
|
||||
};
|
||||
let json = serde_json::json!({
|
||||
"metadata": {
|
||||
"name": "Example Pack",
|
||||
"game_version": "1.18.2",
|
||||
"format_version": 1u32,
|
||||
"loader": "vanilla",
|
||||
},
|
||||
"java": {
|
||||
"extra_arguments": [],
|
||||
"install": "/usr/bin/java",
|
||||
},
|
||||
"memory": {
|
||||
"maximum": 8192u32,
|
||||
},
|
||||
"resolution": (1920u16, 1080u16),
|
||||
"hooks": {},
|
||||
});
|
||||
|
||||
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
|
||||
assert_str_eq!(
|
||||
format!("{:?}", serde_json::from_value::<Profile>(json)?),
|
||||
format!("{:?}", profile),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
119
theseus/src/state/settings.rs
Normal file
119
theseus/src/state/settings.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! Theseus settings file
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
// TODO: convert to semver?
|
||||
const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
// Types
|
||||
/// Global Theseus settings
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub memory: MemorySettings,
|
||||
pub game_resolution: WindowSize,
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub java_8_path: Option<PathBuf>,
|
||||
pub java_17_path: Option<PathBuf>,
|
||||
pub hooks: Hooks,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
java_8_path: None,
|
||||
java_17_path: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 64,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub async fn init(file: &Path) -> crate::Result<Self> {
|
||||
if file.exists() {
|
||||
fs::read(&file)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Error reading settings file: {err}"
|
||||
))
|
||||
})
|
||||
.and_then(|it| {
|
||||
serde_json::from_slice::<Settings>(&it)
|
||||
.map_err(crate::Error::from)
|
||||
})
|
||||
} else {
|
||||
Ok(Settings::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
fs::write(to, serde_json::to_vec_pretty(self)?)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Error saving settings to file: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Minecraft memory settings
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct MemorySettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<u32>,
|
||||
pub maximum: u32,
|
||||
}
|
||||
|
||||
impl Default for MemorySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
minimum: None,
|
||||
maximum: 2048,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Game window size
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct WindowSize(pub u16, pub u16);
|
||||
|
||||
impl Default for WindowSize {
|
||||
fn default() -> Self {
|
||||
Self(854, 480)
|
||||
}
|
||||
}
|
||||
|
||||
/// Game initialization hooks
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Hooks {
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
pub pre_launch: HashSet<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
pub post_exit: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Hooks {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pre_launch: HashSet::<String>::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::<String>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, io};
|
||||
|
||||
use path_clean::PathClean;
|
||||
|
||||
// https://stackoverflow.com/a/54817755
|
||||
pub fn absolute_path(path: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let absolute_path = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
env::current_dir()?.join(path)
|
||||
}
|
||||
.clean();
|
||||
|
||||
Ok(absolute_path)
|
||||
}
|
||||
92
theseus/src/util/fetch.rs
Normal file
92
theseus/src/util/fetch.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! Functions for fetching infromation from the Internet
|
||||
use crate::config::REQWEST_CLIENT;
|
||||
use futures::prelude::*;
|
||||
use std::{collections::LinkedList, convert::TryInto, path::Path, sync::Arc};
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io::AsyncWriteExt,
|
||||
sync::{Semaphore, SemaphorePermit},
|
||||
};
|
||||
|
||||
const FETCH_ATTEMPTS: usize = 3;
|
||||
|
||||
pub async fn fetch<'a>(
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
_permit: &SemaphorePermit<'a>,
|
||||
) -> crate::Result<bytes::Bytes> {
|
||||
let mut attempts = LinkedList::new();
|
||||
for _ in 0..FETCH_ATTEMPTS {
|
||||
attempts.push_back(
|
||||
async {
|
||||
let content = REQWEST_CLIENT.get(url).send().await?;
|
||||
let bytes = content.bytes().await?;
|
||||
|
||||
if let Some(hash) = sha1 {
|
||||
let actual_hash = sha1_async(bytes.clone()).await;
|
||||
if actual_hash != hash {
|
||||
return Err(crate::Error::HashError(
|
||||
actual_hash,
|
||||
String::from(hash),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
log::debug!("Done downloading URL {url}");
|
||||
future::select_ok(attempts).map_ok(|it| it.0).await
|
||||
}
|
||||
|
||||
// This is implemented, as it will be useful in porting modpacks
|
||||
// For now, allow it to be dead code
|
||||
#[allow(dead_code)]
|
||||
pub async fn fetch_mirrors(
|
||||
urls: &[&str],
|
||||
sha1: Option<&str>,
|
||||
permits: u32,
|
||||
sem: &Semaphore,
|
||||
) -> crate::Result<bytes::Bytes> {
|
||||
let _permits = sem.acquire_many(permits).await.unwrap();
|
||||
let sem = Arc::new(Semaphore::new(permits.try_into().unwrap()));
|
||||
|
||||
future::select_ok(urls.into_iter().map(|url| {
|
||||
let sha1 = sha1.map(String::from);
|
||||
let url = String::from(*url);
|
||||
let sem = Arc::clone(&sem);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let permit = sem.acquire().await.unwrap();
|
||||
fetch(&url, sha1.as_deref(), &permit).await
|
||||
})
|
||||
.map(Result::unwrap)
|
||||
.boxed()
|
||||
}))
|
||||
.await
|
||||
.map(|it| it.0)
|
||||
}
|
||||
|
||||
pub async fn write<'a>(
|
||||
path: &Path,
|
||||
bytes: &[u8],
|
||||
_permit: &SemaphorePermit<'a>,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut file = File::create(path).await?;
|
||||
log::debug!("Done writing file {}", path.display());
|
||||
file.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sha1_async(bytes: bytes::Bytes) -> String {
|
||||
tokio::task::spawn_blocking(move || sha1::Sha1::from(bytes).hexdigest())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
3
theseus/src/util/mod.rs
Normal file
3
theseus/src/util/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
//! Theseus utility functions
|
||||
pub mod fetch;
|
||||
pub mod platform;
|
||||
60
theseus/src/util/platform.rs
Normal file
60
theseus/src/util/platform.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Platform-related code
|
||||
use daedalus::minecraft::{Os, OsRule};
|
||||
use regex::Regex;
|
||||
|
||||
// OS detection
|
||||
pub trait OsExt {
|
||||
/// Get the OS of the current system
|
||||
fn native() -> Self;
|
||||
}
|
||||
|
||||
impl OsExt for Os {
|
||||
fn native() -> Self {
|
||||
match std::env::consts::OS {
|
||||
"windows" => Self::Windows,
|
||||
"macos" => Self::Osx,
|
||||
"linux" => Self::Linux,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bit width
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub const ARCH_WIDTH: &str = "64";
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub const ARCH_WIDTH: &str = "32";
|
||||
|
||||
// Platform rule handling
|
||||
pub fn os_rule(rule: &OsRule) -> bool {
|
||||
let mut rule_match = true;
|
||||
|
||||
if let Some(ref arch) = rule.arch {
|
||||
rule_match &= match arch.as_str() {
|
||||
"x86" => cfg!(any(target_arch = "x86", target_arch = "x86_64")),
|
||||
"arm" => cfg!(target_arch = "arm"),
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
rule_match &= &Os::native() == name;
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &=
|
||||
regex.is_match(&sys_info::os_release().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
||||
pub fn classpath_separator() -> &'static str {
|
||||
match Os::native() {
|
||||
Os::Osx | Os::Linux | Os::Unknown => ":",
|
||||
Os::Windows => ";",
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../theseus" }
|
||||
daedalus = "0.1.12"
|
||||
daedalus = {version = "0.1.15", features = ["bincode"]}
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||
futures = "0.3"
|
||||
@@ -18,4 +18,7 @@ dialoguer = "0.10"
|
||||
eyre = "0.6"
|
||||
tabled = "0.5"
|
||||
dirs = "4.0"
|
||||
uuid = {version = "0.8", features = ["v4", "serde"]}
|
||||
uuid = {version = "0.8", features = ["v4", "serde"]}
|
||||
# TODO: merge logging with paris logging
|
||||
pretty_env_logger = "0.4"
|
||||
log = "0.4.14"
|
||||
|
||||
@@ -15,10 +15,12 @@ pub struct Args {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = argh::from_env::<Args>();
|
||||
theseus::init().await?;
|
||||
pretty_env_logger::formatted_builder()
|
||||
.filter_module("theseus", log::LevelFilter::Info)
|
||||
.target(pretty_env_logger::env_logger::Target::Stderr)
|
||||
.init();
|
||||
|
||||
args.dispatch()
|
||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
||||
.and_then(|_| async { Ok(theseus::save().await?) })
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@ use crate::util::{
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use eyre::{ensure, Result};
|
||||
use futures::prelude::*;
|
||||
use paris::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tabled::{Table, Tabled};
|
||||
use theseus::{
|
||||
data::{profiles::PROFILE_JSON_PATH, Metadata, Profile, Profiles},
|
||||
launcher::ModLoader,
|
||||
};
|
||||
use theseus::prelude::*;
|
||||
use tokio::fs;
|
||||
use tokio_stream::{wrappers::ReadDirStream, StreamExt};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
@@ -54,16 +52,19 @@ impl ProfileAdd {
|
||||
);
|
||||
|
||||
let profile = self.profile.canonicalize()?;
|
||||
let json_path = profile.join(PROFILE_JSON_PATH);
|
||||
let json_path = profile.join("profile.json");
|
||||
|
||||
ensure!(
|
||||
json_path.exists(),
|
||||
"Profile json does not exist. Perhaps you wanted `profile init` or `profile fetch`?"
|
||||
);
|
||||
ensure!(
|
||||
Profiles::get().await.unwrap().0.get(&profile).is_none(),
|
||||
!profile::is_managed(&profile).await?,
|
||||
"Profile already managed by Theseus. If the contents of the profile are invalid or missing, the profile can be regenerated using `profile init` or `profile fetch`"
|
||||
);
|
||||
Profiles::insert_from(profile).await?;
|
||||
|
||||
profile::add_path(&profile).await?;
|
||||
State::sync().await?;
|
||||
success!("Profile added!");
|
||||
|
||||
Ok(())
|
||||
@@ -106,13 +107,15 @@ impl ProfileInit {
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
// TODO: validate inputs from args early
|
||||
let state = State::get().await?;
|
||||
|
||||
if self.path.exists() {
|
||||
ensure!(
|
||||
self.path.is_dir(),
|
||||
"Attempted to create profile in something other than a folder!"
|
||||
);
|
||||
ensure!(
|
||||
!self.path.join(PROFILE_JSON_PATH).exists(),
|
||||
!self.path.join("profile.json").exists(),
|
||||
"Profile already exists! Perhaps you want `profile add` instead?"
|
||||
);
|
||||
if ReadDirStream::new(fs::read_dir(&self.path).await?)
|
||||
@@ -138,8 +141,6 @@ impl ProfileInit {
|
||||
&self.path.canonicalize()?.display()
|
||||
);
|
||||
|
||||
let metadata = Metadata::get().await?;
|
||||
|
||||
// TODO: abstract default prompting
|
||||
let name = match &self.name {
|
||||
Some(name) => name.clone(),
|
||||
@@ -157,7 +158,7 @@ impl ProfileInit {
|
||||
let game_version = match &self.game_version {
|
||||
Some(version) => version.clone(),
|
||||
None => {
|
||||
let default = &metadata.minecraft.latest.release;
|
||||
let default = &state.metadata.minecraft.latest.release;
|
||||
|
||||
prompt_async(
|
||||
String::from("Game version"),
|
||||
@@ -206,8 +207,8 @@ impl ProfileInit {
|
||||
};
|
||||
|
||||
let loader_data = match loader {
|
||||
ModLoader::Forge => &metadata.forge,
|
||||
ModLoader::Fabric => &metadata.fabric,
|
||||
ModLoader::Forge => &state.metadata.forge,
|
||||
ModLoader::Fabric => &state.metadata.fabric,
|
||||
_ => eyre::bail!("Could not get manifest for loader {loader}. This is a bug in the CLI!"),
|
||||
};
|
||||
|
||||
@@ -238,8 +239,6 @@ impl ProfileInit {
|
||||
.map(PathBuf::from),
|
||||
};
|
||||
|
||||
// We don't really care if the profile already is managed, as getting this far means that the user probably wanted to re-create a profile
|
||||
drop(metadata);
|
||||
let mut profile =
|
||||
Profile::new(name, game_version, self.path.clone()).await?;
|
||||
|
||||
@@ -251,8 +250,8 @@ impl ProfileInit {
|
||||
profile.with_loader(loader, Some(loader_version));
|
||||
}
|
||||
|
||||
Profiles::insert(profile).await?;
|
||||
Profiles::save().await?;
|
||||
profile::add(profile).await?;
|
||||
State::sync().await?;
|
||||
|
||||
success!(
|
||||
"Successfully created instance, it is now available to use with Theseus!"
|
||||
@@ -294,20 +293,39 @@ impl<'a> From<&'a Profile> for ProfileRow<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Path> for ProfileRow<'a> {
|
||||
fn from(it: &'a Path) -> Self {
|
||||
Self {
|
||||
name: "?",
|
||||
path: it,
|
||||
game_version: "?",
|
||||
loader: &ModLoader::Vanilla,
|
||||
loader_version: "?",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileList {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
let profiles = Profiles::get().await?;
|
||||
let profiles = profiles.0.values().map(ProfileRow::from);
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
let profiles = profiles.0.iter().map(|(path, prof)| {
|
||||
prof.as_ref().map_or_else(
|
||||
|| ProfileRow::from(path.as_path()),
|
||||
ProfileRow::from,
|
||||
)
|
||||
});
|
||||
|
||||
let table = Table::new(profiles).with(tabled::Style::psql()).with(
|
||||
tabled::Modify::new(tabled::Column(1..=1))
|
||||
.with(tabled::MaxWidth::wrapping(40)),
|
||||
);
|
||||
println!("{table}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -329,10 +347,13 @@ impl ProfileRemove {
|
||||
) -> Result<()> {
|
||||
let profile = self.profile.canonicalize()?;
|
||||
info!("Removing profile {} from Theseus", self.profile.display());
|
||||
|
||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||
if Profiles::remove(&profile).await?.is_none() {
|
||||
if !profile::is_managed(&profile).await? {
|
||||
warn!("Profile was not managed by Theseus!");
|
||||
} else {
|
||||
profile::remove(&profile).await?;
|
||||
State::sync().await?;
|
||||
success!("Profile removed!");
|
||||
}
|
||||
} else {
|
||||
@@ -372,24 +393,21 @@ impl ProfileRun {
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
info!("Starting profile at path {}...", self.profile.display());
|
||||
let ref profiles = Profiles::get().await?.0;
|
||||
let path = self.profile.canonicalize()?;
|
||||
let profile = profiles
|
||||
.get(&path)
|
||||
.ok_or(
|
||||
eyre::eyre!(
|
||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)"
|
||||
)
|
||||
)?;
|
||||
|
||||
let credentials = theseus::launcher::Credentials {
|
||||
ensure!(
|
||||
profile::is_managed(&path).await?,
|
||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
|
||||
);
|
||||
|
||||
let credentials = Credentials {
|
||||
id: self.id.clone(),
|
||||
username: self.name.clone(),
|
||||
access_token: self.token.clone(),
|
||||
};
|
||||
|
||||
let mut proc = profile.run(&credentials).await?;
|
||||
profile.wait_for(&mut proc).await?;
|
||||
let mut proc = profile::run(&path, &credentials).await?;
|
||||
profile::wait_for(&mut proc).await?;
|
||||
|
||||
success!("Process exited successfully!");
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user