Profile bindings (#55)
* basic framework. still has errors * added functionality for main endpoints + some structuring * formatting * unused code * mimicked CLI function with wait_for process * made PR changes, added playground * cargo fmt * removed missed println * misc tests fixes * cargo fmt * added windows support * cargo fmt * all OS use dunce * restructured profile slightly; fixed mac bug * profile changes, new main.rs * fixed requested pr + canonicaliation bug * fixed regressed bug in ui * fixed regressed bugs * fixed git error * typo * ran prettier * clippy * playground clippy * ported profile loading fix * profile change for real, url println and clippy * PR changes --------- Co-authored-by: Wyatt <wyatt@modrinth.com>
This commit is contained in:
parent
24ba986cf1
commit
f48959a816
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,6 +8,9 @@ WixTools
|
||||
.DS_Store
|
||||
.pnpm-debug.log
|
||||
|
||||
minecraft
|
||||
config
|
||||
|
||||
[#]*[#]
|
||||
|
||||
# TEMPORARY: ignore my test instance and metadata
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -3531,6 +3531,7 @@ dependencies = [
|
||||
"sys-info",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"toml 0.7.3",
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
@ -3549,6 +3550,7 @@ dependencies = [
|
||||
"daedalus",
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"eyre",
|
||||
"futures",
|
||||
"paris",
|
||||
@ -3563,16 +3565,41 @@ dependencies = [
|
||||
"url",
|
||||
"uuid 1.3.0",
|
||||
"webbrowser",
|
||||
"winreg 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theseus_gui"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"daedalus",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"theseus",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theseus_playground"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"daedalus",
|
||||
"dunce",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"theseus",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"webbrowser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
members = [
|
||||
"theseus",
|
||||
"theseus_cli",
|
||||
"theseus_playground",
|
||||
"theseus_gui/src-tauri"
|
||||
]
|
||||
|
||||
|
||||
@ -36,11 +36,13 @@ futures = "0.3"
|
||||
once_cell = "1.9.0"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
dunce = "1.0.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.11.0"
|
||||
dunce = "1.0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
argh = "0.1.6"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
//! API for interacting with Theseus
|
||||
pub mod auth;
|
||||
pub mod profile;
|
||||
pub mod profile_create;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::state::{
|
||||
@ -14,6 +15,6 @@ pub mod prelude {
|
||||
auth::{self, Credentials},
|
||||
data::*,
|
||||
profile::{self, Profile},
|
||||
State,
|
||||
profile_create, State,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,8 +7,12 @@ use daedalus as d;
|
||||
use std::{
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
process::{Child, Command},
|
||||
sync::RwLock,
|
||||
};
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
/// Add a profile to the in-memory state
|
||||
#[tracing::instrument]
|
||||
@ -105,11 +109,12 @@ pub async fn list(
|
||||
}
|
||||
|
||||
/// Run Minecraft using a profile
|
||||
/// Returns Arc pointer to RwLock to Child
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn run(
|
||||
path: &Path,
|
||||
credentials: &crate::auth::Credentials,
|
||||
) -> crate::Result<Child> {
|
||||
) -> crate::Result<Arc<RwLock<Child>>> {
|
||||
let state = State::get().await.unwrap();
|
||||
let settings = state.settings.read().await;
|
||||
let profile = get(path).await?.ok_or_else(|| {
|
||||
@ -199,7 +204,7 @@ pub async fn run(
|
||||
let memory = profile.memory.unwrap_or(settings.memory);
|
||||
let resolution = profile.resolution.unwrap_or(settings.game_resolution);
|
||||
|
||||
crate::launcher::launch_minecraft(
|
||||
let mc_process = crate::launcher::launch_minecraft(
|
||||
&profile.metadata.game_version,
|
||||
&profile.metadata.loader_version,
|
||||
&profile.path,
|
||||
@ -210,7 +215,18 @@ pub async fn run(
|
||||
&resolution,
|
||||
credentials,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
// Insert child into state
|
||||
let mut state_children = state.children.write().await;
|
||||
let pid = mc_process.id().ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(
|
||||
"Process failed to stay open.".to_string(),
|
||||
)
|
||||
})?;
|
||||
let child_arc = state_children.insert(pid, mc_process);
|
||||
|
||||
Ok(child_arc)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
||||
165
theseus/src/api/profile_create.rs
Normal file
165
theseus/src/api/profile_create.rs
Normal file
@ -0,0 +1,165 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::{prelude::ModLoader, profile};
|
||||
pub use crate::{
|
||||
state::{JavaSettings, Profile},
|
||||
State,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use dunce::canonicalize;
|
||||
use futures::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_NAME: &str = "Untitled Instance";
|
||||
|
||||
// Generic basic profile creation tool.
|
||||
// Creates an essentially empty dummy profile with profile_create
|
||||
#[tracing::instrument]
|
||||
pub async fn profile_create_empty() -> crate::Result<PathBuf> {
|
||||
profile_create(
|
||||
String::from(DEFAULT_NAME), // the name/path of the profile
|
||||
String::from("1.19.2"), // the game version of the profile
|
||||
ModLoader::Vanilla, // the modloader to use
|
||||
String::from("stable"), // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||
None, // the icon for the profile
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Creates a profile at the given filepath and adds it to the in-memory state
|
||||
// Returns filepath at which it can be accessed in the State
|
||||
#[tracing::instrument]
|
||||
pub async fn profile_create(
|
||||
name: String, // the name of the profile, and relative path
|
||||
game_version: String, // the game version of the profile
|
||||
modloader: ModLoader, // the modloader to use
|
||||
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||
icon: Option<PathBuf>, // the icon for the profile
|
||||
) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
let path = state.directories.profiles_dir().join(uuid.to_string());
|
||||
|
||||
if path.exists() {
|
||||
if !path.is_dir() {
|
||||
return Err(ProfileCreationError::NotFolder.into());
|
||||
}
|
||||
if path.join("profile.json").exists() {
|
||||
return Err(ProfileCreationError::ProfileExistsError(
|
||||
path.join("profile.json"),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if ReadDirStream::new(fs::read_dir(&path).await?)
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err(ProfileCreationError::NotEmptyFolder.into());
|
||||
}
|
||||
} else {
|
||||
fs::create_dir_all(&path).await?;
|
||||
}
|
||||
println!(
|
||||
"Creating profile at path {}",
|
||||
&canonicalize(&path)?.display()
|
||||
);
|
||||
|
||||
let loader = modloader;
|
||||
let loader = if loader != ModLoader::Vanilla {
|
||||
let version = loader_version;
|
||||
|
||||
let filter = |it: &LoaderVersion| match version.as_str() {
|
||||
"latest" => true,
|
||||
"stable" => it.stable,
|
||||
id => it.id == *id,
|
||||
};
|
||||
|
||||
let loader_data = match loader {
|
||||
ModLoader::Forge => &state.metadata.forge,
|
||||
ModLoader::Fabric => &state.metadata.fabric,
|
||||
_ => {
|
||||
return Err(ProfileCreationError::NoManifest(
|
||||
loader.to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let loaders = &loader_data
|
||||
.game_versions
|
||||
.iter()
|
||||
.find(|it| it.id == game_version)
|
||||
.ok_or_else(|| {
|
||||
ProfileCreationError::ModloaderUnsupported(
|
||||
loader.to_string(),
|
||||
game_version.clone(),
|
||||
)
|
||||
})?
|
||||
.loaders;
|
||||
|
||||
let loader_version = loaders
|
||||
.iter()
|
||||
.cloned()
|
||||
.find(filter)
|
||||
.or(
|
||||
// If stable was searched for but not found, return latest by default
|
||||
if version == "stable" {
|
||||
loaders.iter().next().cloned()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
ProfileCreationError::InvalidVersionModloader(
|
||||
version,
|
||||
loader.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Some((loader_version, loader))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fully canonicalize now that its created for storing purposes
|
||||
let path = canonicalize(&path)?;
|
||||
let mut profile = Profile::new(name, game_version, path.clone()).await?;
|
||||
if let Some(ref icon) = icon {
|
||||
profile.with_icon(icon).await?;
|
||||
}
|
||||
if let Some((loader_version, loader)) = loader {
|
||||
profile.with_loader(loader, Some(loader_version));
|
||||
}
|
||||
|
||||
profile::add(profile).await?;
|
||||
State::sync().await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ProfileCreationError {
|
||||
#[error("Profile .json exists: {0}")]
|
||||
ProfileExistsError(PathBuf),
|
||||
#[error("Modloader {0} unsupported for Minecraft version {1}")]
|
||||
ModloaderUnsupported(String, String),
|
||||
#[error("Invalid version {0} for modloader {1}")]
|
||||
InvalidVersionModloader(String, String),
|
||||
#[error("Could not get manifest for loader {0}. This is a bug in the GUI")]
|
||||
NoManifest(String),
|
||||
#[error("Could not get State.")]
|
||||
NoState,
|
||||
|
||||
#[error("Attempted to create project in something other than a folder.")]
|
||||
NotFolder,
|
||||
#[error("You are trying to create a profile in a non-empty directory")]
|
||||
NotEmptyFolder,
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
//! Theseus error type
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
use crate::profile_create;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
#[error("Filesystem error: {0}")]
|
||||
@ -57,6 +59,9 @@ pub enum ErrorKind {
|
||||
#[error("Invalid input: {0}")]
|
||||
InputError(String),
|
||||
|
||||
#[error("Recv error: {0}")]
|
||||
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
|
||||
|
||||
#[error(
|
||||
"Tried to access unloaded profile {0}, loading it probably failed"
|
||||
)]
|
||||
@ -65,6 +70,9 @@ pub enum ErrorKind {
|
||||
#[error("Profile {0} is not managed by Theseus!")]
|
||||
UnmanagedProfileError(String),
|
||||
|
||||
#[error("Could not create profile: {0}")]
|
||||
ProfileCreationError(#[from] profile_create::ProfileCreationError),
|
||||
|
||||
#[error("Error: {0}")]
|
||||
OtherError(String),
|
||||
}
|
||||
|
||||
@ -10,10 +10,14 @@ use daedalus::{
|
||||
minecraft::{Argument, ArgumentValue, Library, VersionType},
|
||||
modded::SidedDataEntry,
|
||||
};
|
||||
use dunce::canonicalize;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Replaces the space separator with a newline character, as to not split the arguments
|
||||
const TEMPORARY_REPLACE_CHAR: &str = "\n";
|
||||
|
||||
pub fn get_class_paths(
|
||||
libraries_path: &Path,
|
||||
libraries: &[Library],
|
||||
@ -37,8 +41,7 @@ pub fn get_class_paths(
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
cps.push(
|
||||
client_path
|
||||
.canonicalize()
|
||||
canonicalize(client_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
@ -70,7 +73,7 @@ pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
||||
|
||||
path.push(get_path_from_artifact(lib)?);
|
||||
|
||||
let path = &path.canonicalize().map_err(|_| {
|
||||
let path = &canonicalize(&path).map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Library file at path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
@ -104,15 +107,13 @@ pub fn get_jvm_arguments(
|
||||
} else {
|
||||
parsed_arguments.push(format!(
|
||||
"-Djava.library.path={}",
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
canonicalize(natives_path)
|
||||
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
.as_error())?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
));
|
||||
parsed_arguments.push("-cp".to_string());
|
||||
parsed_arguments.push(class_paths.to_string());
|
||||
@ -142,8 +143,7 @@ fn parse_jvm_argument(
|
||||
Ok(argument
|
||||
.replace(
|
||||
"${natives_directory}",
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
&canonicalize(natives_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
@ -155,8 +155,7 @@ fn parse_jvm_argument(
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
&libraries_path
|
||||
.canonicalize()
|
||||
&canonicalize(libraries_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified libraries path {} does not exist",
|
||||
@ -206,7 +205,7 @@ pub fn get_minecraft_arguments(
|
||||
Ok(parsed_arguments)
|
||||
} else if let Some(legacy_arguments) = legacy_arguments {
|
||||
Ok(parse_minecraft_argument(
|
||||
legacy_arguments,
|
||||
&legacy_arguments.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||
&credentials.access_token,
|
||||
&credentials.username,
|
||||
&credentials.id,
|
||||
@ -249,8 +248,7 @@ fn parse_minecraft_argument(
|
||||
.replace("${assets_index_name}", asset_index_name)
|
||||
.replace(
|
||||
"${game_directory}",
|
||||
&game_directory
|
||||
.canonicalize()
|
||||
&canonicalize(game_directory)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified game directory {} does not exist",
|
||||
@ -262,8 +260,7 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
&canonicalize(assets_directory)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
@ -275,8 +272,7 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
&canonicalize(assets_directory)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
@ -302,9 +298,9 @@ where
|
||||
for argument in arguments {
|
||||
match argument {
|
||||
Argument::Normal(arg) => {
|
||||
let parsed = parse_function(arg)?;
|
||||
|
||||
for arg in parsed.split(' ') {
|
||||
let parsed =
|
||||
parse_function(&arg.replace(' ', TEMPORARY_REPLACE_CHAR))?;
|
||||
for arg in parsed.split(TEMPORARY_REPLACE_CHAR) {
|
||||
parsed_arguments.push(arg.to_string());
|
||||
}
|
||||
}
|
||||
@ -312,11 +308,15 @@ where
|
||||
if rules.iter().all(parse_rule) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed_arguments.push(parse_function(arg)?);
|
||||
parsed_arguments.push(parse_function(
|
||||
&arg.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||
)?);
|
||||
}
|
||||
ArgumentValue::Many(args) => {
|
||||
for arg in args {
|
||||
parsed_arguments.push(parse_function(arg)?);
|
||||
parsed_arguments.push(parse_function(
|
||||
&arg.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ use bincode::{Decode, Encode};
|
||||
use chrono::{prelude::*, Duration};
|
||||
use futures::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
lazy_static! {
|
||||
@ -47,7 +47,7 @@ struct ProfileInfoJSON {
|
||||
}
|
||||
|
||||
// Login information
|
||||
#[derive(Encode, Decode)]
|
||||
#[derive(Encode, Decode, Serialize, Deserialize)]
|
||||
pub struct Credentials {
|
||||
#[bincode(with_serde)]
|
||||
pub id: uuid::Uuid,
|
||||
|
||||
@ -139,10 +139,9 @@ pub async fn download_assets(
|
||||
index: &AssetsIndex,
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading assets");
|
||||
|
||||
stream::iter(index.objects.iter())
|
||||
.map(Ok::<(&String, &Asset), crate::Error>)
|
||||
.try_for_each_concurrent(None, |(name, asset)| async move {
|
||||
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |(name, asset)| async move {
|
||||
let hash = &asset.hash;
|
||||
let resource_path = st.directories.object_dir(hash);
|
||||
let url = format!(
|
||||
@ -202,7 +201,7 @@ pub async fn download_libraries(
|
||||
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>)
|
||||
.try_for_each_concurrent(None, |library| async move {
|
||||
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |library| async move {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !rules.iter().all(super::parse_rule) {
|
||||
return Ok(());
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
//! Logic for launching Minecraft
|
||||
use crate::state as st;
|
||||
use daedalus as d;
|
||||
use dunce::canonicalize;
|
||||
use std::{path::Path, process::Stdio};
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
@ -58,7 +59,7 @@ pub async fn launch_minecraft(
|
||||
credentials: &auth::Credentials,
|
||||
) -> crate::Result<Child> {
|
||||
let state = st::State::get().await?;
|
||||
let instance_path = instance_path.canonicalize()?;
|
||||
let instance_path = &canonicalize(instance_path)?;
|
||||
|
||||
let version = state
|
||||
.metadata
|
||||
@ -173,34 +174,45 @@ pub async fn launch_minecraft(
|
||||
};
|
||||
|
||||
command
|
||||
.args(args::get_jvm_arguments(
|
||||
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(
|
||||
.args(
|
||||
args::get_jvm_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||
.map(|x| x.as_slice()),
|
||||
&state.directories.version_natives_dir(&version.id),
|
||||
&state.directories.libraries_dir(),
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
Vec::from(java_args),
|
||||
)?)
|
||||
&args::get_class_paths(
|
||||
&state.directories.libraries_dir(),
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
Vec::from(java_args),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|r| r.replace(' ', r"\ "))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.arg(version_info.main_class.clone())
|
||||
.args(args::get_minecraft_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Game)
|
||||
.map(|x| x.as_slice()),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version_info.asset_index.id,
|
||||
&instance_path,
|
||||
&state.directories.assets_dir(),
|
||||
&version.type_,
|
||||
*resolution,
|
||||
)?)
|
||||
.args(
|
||||
args::get_minecraft_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Game)
|
||||
.map(|x| x.as_slice()),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version_info.asset_index.id,
|
||||
instance_path,
|
||||
&state.directories.assets_dir(),
|
||||
&version.type_,
|
||||
*resolution,
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|r| r.replace(' ', r"\ "))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.current_dir(instance_path.clone())
|
||||
.env_clear()
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
|
||||
37
theseus/src/state/children.rs
Normal file
37
theseus/src/state/children.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::process::Child;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// Child processes (instances of Minecraft)
|
||||
// A wrapper over a Hashmap connecting PID -> Child
|
||||
// Left open for future functionality re: polling children
|
||||
pub struct Children(HashMap<u32, Arc<RwLock<Child>>>);
|
||||
|
||||
impl Children {
|
||||
pub fn new() -> Children {
|
||||
Children(HashMap::new())
|
||||
}
|
||||
|
||||
// Inserts and returns a ref to the child
|
||||
// Unlike a Hashmap, this directly returns the reference to the Child rather than any previously stored Child that may exist
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
pid: u32,
|
||||
child: tokio::process::Child,
|
||||
) -> Arc<RwLock<Child>> {
|
||||
let child = Arc::new(RwLock::new(child));
|
||||
self.0.insert(pid, child.clone());
|
||||
child
|
||||
}
|
||||
|
||||
// Returns a ref to the child
|
||||
pub fn get(&self, pid: &u32) -> Option<Arc<RwLock<Child>>> {
|
||||
self.0.get(pid).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Children {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@ -110,6 +110,12 @@ impl DirectoryInfo {
|
||||
self.config_dir.join("icons")
|
||||
}
|
||||
|
||||
/// Get the profiles directory for created profiles
|
||||
#[inline]
|
||||
pub fn profiles_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("profiles")
|
||||
}
|
||||
|
||||
/// Get the file containing the global database
|
||||
#[inline]
|
||||
pub fn database_file(&self) -> PathBuf {
|
||||
|
||||
@ -22,6 +22,9 @@ pub use self::projects::*;
|
||||
mod users;
|
||||
pub use self::users::*;
|
||||
|
||||
mod children;
|
||||
pub use self::children::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
pub struct State {
|
||||
@ -36,6 +39,8 @@ pub struct State {
|
||||
// TODO: settings API
|
||||
/// Launcher configuration
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Reference to process children
|
||||
pub children: RwLock<Children>,
|
||||
/// Launcher profile metadata
|
||||
pub(crate) profiles: RwLock<Profiles>,
|
||||
/// Launcher user account info
|
||||
@ -73,6 +78,8 @@ impl State {
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
let children = Children::new();
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
@ -81,6 +88,7 @@ impl State {
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ use crate::config::BINCODE_CONFIG;
|
||||
use crate::data::DirectoryInfo;
|
||||
use crate::state::projects::Project;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use dunce::canonicalize;
|
||||
use futures::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@ -98,7 +99,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path: path.canonicalize()?,
|
||||
path: canonicalize(path)?,
|
||||
metadata: ProfileMetadata {
|
||||
name,
|
||||
icon: None,
|
||||
@ -236,10 +237,13 @@ impl Profiles {
|
||||
{
|
||||
for (profile_path, _profile_opt) in profiles.iter() {
|
||||
let mut read_paths = |path: &str| {
|
||||
for path in std::fs::read_dir(profile_path.join(path))? {
|
||||
files.insert(path?.path(), profile_path.clone());
|
||||
let new_path = profile_path.join(path);
|
||||
if new_path.exists() {
|
||||
for path in std::fs::read_dir(profile_path.join(path))?
|
||||
{
|
||||
files.insert(path?.path(), profile_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
};
|
||||
read_paths("mods")?;
|
||||
@ -268,9 +272,7 @@ impl Profiles {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
self.0.insert(
|
||||
profile
|
||||
.path
|
||||
.canonicalize()?
|
||||
canonicalize(&profile.path)?
|
||||
.to_str()
|
||||
.ok_or(
|
||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||
@ -286,12 +288,12 @@ impl Profiles {
|
||||
&'a mut self,
|
||||
path: &'a Path,
|
||||
) -> crate::Result<&Self> {
|
||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
||||
self.insert(Self::read_profile_from_dir(&canonicalize(path)?).await?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
||||
let path = PathBuf::from(&canonicalize(path)?.to_str().unwrap());
|
||||
self.0.remove(&path);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use dunce::canonicalize;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
@ -11,12 +12,6 @@ use winreg::{
|
||||
RegKey,
|
||||
};
|
||||
|
||||
// Uses dunce canonicalization to resolve symlinks without UNC prefixes
|
||||
#[cfg(target_os = "windows")]
|
||||
use dunce::canonicalize;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::fs::canonicalize;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct JavaVersion {
|
||||
pub path: String,
|
||||
|
||||
@ -26,5 +26,9 @@ tracing = "0.1"
|
||||
tracing-error = "0.2"
|
||||
tracing-futures = "0.2"
|
||||
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
||||
dunce = "1.0.3"
|
||||
|
||||
webbrowser = "0.7"
|
||||
webbrowser = "0.7"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.11.0"
|
||||
|
||||
@ -3,6 +3,7 @@ use crate::util::{
|
||||
confirm_async, prompt_async, select_async, table, table_path_display,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use dunce::canonicalize;
|
||||
use eyre::{ensure, Result};
|
||||
use futures::prelude::*;
|
||||
use paris::*;
|
||||
@ -50,7 +51,7 @@ impl ProfileAdd {
|
||||
self.profile.display()
|
||||
);
|
||||
|
||||
let profile = self.profile.canonicalize()?;
|
||||
let profile = canonicalize(&self.profile)?;
|
||||
let json_path = profile.join("profile.json");
|
||||
|
||||
ensure!(
|
||||
@ -137,7 +138,7 @@ impl ProfileInit {
|
||||
}
|
||||
info!(
|
||||
"Creating profile at path {}",
|
||||
&self.path.canonicalize()?.display()
|
||||
&canonicalize(&self.path)?.display()
|
||||
);
|
||||
|
||||
// TODO: abstract default prompting
|
||||
@ -343,7 +344,7 @@ impl ProfileRemove {
|
||||
_args: &crate::Args,
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
let profile = self.profile.canonicalize()?;
|
||||
let profile = canonicalize(&self.profile)?;
|
||||
info!("Removing profile {} from Theseus", self.profile.display());
|
||||
|
||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||
@ -382,7 +383,7 @@ impl ProfileRun {
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
info!("Starting profile at path {}...", self.profile.display());
|
||||
let path = self.profile.canonicalize()?;
|
||||
let path = canonicalize(&self.profile)?;
|
||||
|
||||
ensure!(
|
||||
profile::is_managed(&path).await?,
|
||||
@ -402,7 +403,8 @@ impl ProfileRun {
|
||||
.await?;
|
||||
let credentials = auth::refresh(id, false).await?;
|
||||
|
||||
let mut proc = profile::run(&path, &credentials).await?;
|
||||
let proc_lock = profile::run(&path, &credentials).await?;
|
||||
let mut proc = proc_lock.write().await;
|
||||
profile::wait_for(&mut proc).await?;
|
||||
|
||||
success!("Process exited successfully!");
|
||||
|
||||
2
theseus_gui/.gitignore
vendored
2
theseus_gui/.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.minecraft
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
@ -13,9 +13,17 @@ edition = "2021"
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../../theseus" }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["shell-open"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
|
||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||
futures = "0.3"
|
||||
daedalus = {version = "0.1.15", features = ["bincode"] }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
66
theseus_gui/src-tauri/src/api/mod.rs
Normal file
66
theseus_gui/src-tauri/src/api/mod.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod profile;
|
||||
pub mod profile_create;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TheseusGuiError>;
|
||||
|
||||
// Main returnable Theseus GUI error
|
||||
// Needs to be Serializable to be returned to the JavaScript side
|
||||
#[derive(Error, Debug, Serialize)]
|
||||
pub enum TheseusGuiError {
|
||||
#[error(transparent)]
|
||||
Serializable(TheseusSerializableError),
|
||||
}
|
||||
|
||||
// Serializable error intermediary, so TheseusGuiError can be Serializable (eg: so that we can return theseus::Errors in Tauri directly)
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TheseusSerializableError {
|
||||
#[error("Theseus API error: {0}")]
|
||||
Theseus(#[from] theseus::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
// Generic implementation of From<T> for ErrorTypeA
|
||||
impl<T> From<T> for TheseusGuiError
|
||||
where
|
||||
TheseusSerializableError: From<T>,
|
||||
{
|
||||
fn from(error: T) -> Self {
|
||||
TheseusGuiError::Serializable(TheseusSerializableError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
// This is a very simple macro that implements a very basic Serializable for each variant of TheseusSerializableError,
|
||||
// where the field is the string. (This allows easy extension to errors without many match arms)
|
||||
macro_rules! impl_serialize {
|
||||
($($variant:ident),* $(,)?) => {
|
||||
impl Serialize for TheseusSerializableError {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
$(
|
||||
TheseusSerializableError::$variant(message) => {
|
||||
let mut state = serializer.serialize_struct(stringify!($variant), 2)?;
|
||||
state.serialize_field("field_name", stringify!($variant))?;
|
||||
state.serialize_field("message", &message.to_string())?;
|
||||
state.end()
|
||||
},
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Use the macro to implement Serialize for TheseusSerializableError
|
||||
impl_serialize! {
|
||||
Theseus,
|
||||
IO,
|
||||
}
|
||||
124
theseus_gui/src-tauri/src/api/profile.rs
Normal file
124
theseus_gui/src-tauri/src/api/profile.rs
Normal file
@ -0,0 +1,124 @@
|
||||
use crate::api::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use theseus::prelude::*;
|
||||
|
||||
// Add a profile to the in-memory state
|
||||
// invoke('profile_add',profile)
|
||||
#[tauri::command]
|
||||
pub async fn profile_add(profile: Profile) -> Result<()> {
|
||||
let res = profile::add(profile).await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Add a path as a profile in-memory
|
||||
// invoke('profile_add_path',path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_add_path(path: &Path) -> Result<()> {
|
||||
let res = profile::add_path(path).await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Remove a profile
|
||||
// invoke('profile_add_path',path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_remove(path: &Path) -> Result<()> {
|
||||
let res = profile::remove(path).await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Get a profile by path
|
||||
// invoke('profile_add_path',path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_get(path: &Path) -> Result<Option<Profile>> {
|
||||
let res = profile::get(path).await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Check if a profile is already managed by Theseus
|
||||
// invoke('profile_is_managed',profile)
|
||||
#[tauri::command]
|
||||
pub async fn profile_is_managed(profile: &Path) -> Result<bool> {
|
||||
let res = profile::is_managed(profile).await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Check if a profile is loaded
|
||||
// invoke('profile_is_loaded',profile)
|
||||
#[tauri::command]
|
||||
pub async fn profile_is_loaded(profile: &Path) -> Result<bool> {
|
||||
let res = profile::is_loaded(profile).await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Get a copy of the profile set
|
||||
// invoke('profile_list')
|
||||
#[tauri::command]
|
||||
pub async fn profile_list(
|
||||
) -> Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
|
||||
let res = profile::list().await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Run Minecraft using a profile
|
||||
// Returns a u32 representing the PID, which can be used to poll
|
||||
// for the actual Child in the state.
|
||||
// invoke('profile_run')
|
||||
#[tauri::command]
|
||||
pub async fn profile_run(
|
||||
path: &Path,
|
||||
credentials: theseus::auth::Credentials,
|
||||
) -> Result<u32> {
|
||||
let proc_lock = profile::run(path, &credentials).await?;
|
||||
let pid = proc_lock.read().await.id().ok_or_else(|| {
|
||||
theseus::Error::from(theseus::ErrorKind::LauncherError(format!(
|
||||
"Process failed to stay open."
|
||||
)))
|
||||
})?;
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
// Run Minecraft using a profile, and wait for the result
|
||||
// invoke('profile_wait_for', path, credentials)
|
||||
#[tauri::command]
|
||||
pub async fn profile_run_wait(
|
||||
path: &Path,
|
||||
credentials: theseus::auth::Credentials,
|
||||
) -> Result<()> {
|
||||
let proc_lock = profile::run(path, &credentials).await?;
|
||||
let mut proc = proc_lock.write().await;
|
||||
Ok(profile::wait_for(&mut proc).await?)
|
||||
}
|
||||
|
||||
// Wait for a running minecraft process (a Child)
|
||||
// invoke('profile_wait_for', pid)
|
||||
#[tauri::command]
|
||||
pub async fn profile_wait_for(pid: u32) -> Result<()> {
|
||||
let st = State::get().await?;
|
||||
if let Some(proc_lock) = st.children.blocking_read().get(&pid) {
|
||||
let mut proc = proc_lock.write().await;
|
||||
return Ok(profile::wait_for(&mut proc).await?);
|
||||
}
|
||||
// If child is gone from state, it's not tracked or already finished
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tries to kill a running minecraft process (if PID is still stored)
|
||||
// invoke('profile_kill', pid)
|
||||
#[tauri::command]
|
||||
pub async fn profile_kill(pid: u32) -> Result<()> {
|
||||
let st = State::get().await?;
|
||||
let st = State::get().await?;
|
||||
if let Some(proc_lock) = st.children.blocking_read().get(&pid) {
|
||||
let mut proc = proc_lock.write().await;
|
||||
return Ok(profile::kill(&mut proc).await?);
|
||||
}
|
||||
// If child is gone from state, it's not tracked or already finished
|
||||
Ok(())
|
||||
}
|
||||
34
theseus_gui/src-tauri/src/api/profile_create.rs
Normal file
34
theseus_gui/src-tauri/src/api/profile_create.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use crate::api::Result;
|
||||
use std::path::PathBuf;
|
||||
use theseus::prelude::*;
|
||||
|
||||
// Generic basic profile creation tool.
|
||||
// Creates an essentially empty dummy profile with profile_create
|
||||
#[tauri::command]
|
||||
pub async fn profile_create_empty() -> Result<PathBuf> {
|
||||
let res = profile_create::profile_create_empty().await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Creates a profile at the given filepath and adds it to the in-memory state
|
||||
// invoke('profile_add',profile)
|
||||
#[tauri::command]
|
||||
pub async fn profile_create(
|
||||
name: String, // the name of the profile, and relative path
|
||||
game_version: String, // the game version of the profile
|
||||
modloader: ModLoader, // the modloader to use
|
||||
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||
icon: Option<PathBuf>, // the icon for the profile
|
||||
) -> Result<PathBuf> {
|
||||
let res = profile_create::profile_create(
|
||||
name,
|
||||
game_version,
|
||||
modloader,
|
||||
loader_version,
|
||||
icon,
|
||||
)
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
Ok(res)
|
||||
}
|
||||
@ -3,8 +3,33 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use theseus::prelude::*;
|
||||
|
||||
mod api;
|
||||
|
||||
// Should be called in launcher initialization
|
||||
#[tauri::command]
|
||||
async fn initialize_state() -> api::Result<()> {
|
||||
State::get().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
initialize_state,
|
||||
api::profile_create::profile_create_empty,
|
||||
api::profile_create::profile_create,
|
||||
api::profile::profile_add,
|
||||
api::profile::profile_add_path,
|
||||
api::profile::profile_remove,
|
||||
api::profile::profile_get,
|
||||
api::profile::profile_is_managed,
|
||||
api::profile::profile_is_loaded,
|
||||
api::profile::profile_list,
|
||||
api::profile::profile_run,
|
||||
api::profile::profile_run_wait,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
73
theseus_gui/src/helpers/profile.js
Normal file
73
theseus_gui/src/helpers/profile.js
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
// Add empty default instance
|
||||
export async function addDefaultInstance() {
|
||||
return await invoke('profile_create_empty')
|
||||
}
|
||||
|
||||
// Add empty default instance
|
||||
export async function create() {
|
||||
return await invoke('profile_create')
|
||||
}
|
||||
|
||||
// Add a profile to the in-memory state
|
||||
export async function add(profile) {
|
||||
return await invoke('profile_add', profile)
|
||||
}
|
||||
|
||||
// Add a path as a profile in-memory
|
||||
export async function add_path(path) {
|
||||
return await invoke('profile_add_path', path)
|
||||
}
|
||||
|
||||
// Remove a profile
|
||||
export async function remove(path) {
|
||||
return await invoke('profile_remove', path)
|
||||
}
|
||||
|
||||
// Get a profile by path
|
||||
export async function get(path) {
|
||||
return await invoke('profile_get', path)
|
||||
}
|
||||
|
||||
// Check if a pathed profile is already managed by Theseus
|
||||
export async function is_managed(path) {
|
||||
return await invoke('profile_is_managed', path)
|
||||
}
|
||||
|
||||
// Check if a pathed profile is loaded
|
||||
export async function is_loaded(path) {
|
||||
return await invoke('profile_is_loaded', path)
|
||||
}
|
||||
|
||||
// Get a copy of the profile set
|
||||
export async function list() {
|
||||
return await invoke('profile_list')
|
||||
}
|
||||
|
||||
// Run Minecraft using a pathed profile
|
||||
// Returns PID of child
|
||||
export async function run(path, credentials) {
|
||||
return await invoke('profile_run', path, credentials)
|
||||
}
|
||||
|
||||
// Run Minecraft using a pathed profile
|
||||
// Waits for end
|
||||
export async function run_wait(path, credentials) {
|
||||
return await invoke('run_wait', path, credentials)
|
||||
}
|
||||
|
||||
// Tries to kill a running minecraft process (if PID is still stored)
|
||||
export async function kill(child_pid) {
|
||||
return await invoke('profile_kill', child_pid)
|
||||
}
|
||||
|
||||
// Wait for a running minecraft process (a Child)
|
||||
export async function wait_for(child_pid) {
|
||||
return await invoke('profile_wait_for', child_pid)
|
||||
}
|
||||
12
theseus_gui/src/helpers/state.js
Normal file
12
theseus_gui/src/helpers/state.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
// Initialize the theseus API state
|
||||
// This should be called during the initializion/opening of the launcher
|
||||
export async function initialize_state() {
|
||||
return await invoke('initialize_state')
|
||||
}
|
||||
22
theseus_playground/Cargo.toml
Normal file
22
theseus_playground/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "theseus_playground"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../theseus" }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["shell-open"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
url = "2.2"
|
||||
webbrowser = "0.7"
|
||||
dunce = "1.0.3"
|
||||
|
||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||
futures = "0.3"
|
||||
daedalus = {version = "0.1.15", features = ["bincode"] }
|
||||
119
theseus_playground/src/main.rs
Normal file
119
theseus_playground/src/main.rs
Normal file
@ -0,0 +1,119 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use dunce::canonicalize;
|
||||
use std::path::Path;
|
||||
use theseus::{prelude::*, profile_create::profile_create};
|
||||
use tokio::process::Child;
|
||||
use tokio::sync::{oneshot, RwLockWriteGuard};
|
||||
|
||||
// We use this function directly to call authentication procedure
|
||||
// Note: "let url = match url" logic is handled differently, so that if there is a rate limit in the other set causing that one to end early,
|
||||
// we can see the error message in this thread rather than a Recv error on 'rx' when the receiver is mysteriously droppped
|
||||
pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||
println!("Adding new user account to Theseus");
|
||||
println!("A browser window will now open, follow the login flow there.");
|
||||
|
||||
let (tx, rx) = oneshot::channel::<url::Url>();
|
||||
let flow = tokio::spawn(auth::authenticate(tx));
|
||||
|
||||
let url = rx.await;
|
||||
let url = match url {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
flow.await.unwrap()?;
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
println!("URL {}", url.as_str());
|
||||
webbrowser::open(url.as_str())?;
|
||||
let credentials = flow.await.unwrap()?;
|
||||
State::sync().await?;
|
||||
println!("Logged in user {}.", credentials.username);
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> theseus::Result<()> {
|
||||
println!("Starting.");
|
||||
|
||||
// Initialize state
|
||||
let st = State::get().await?;
|
||||
|
||||
// Set max concurrent downloads to 10
|
||||
st.settings.write().await.max_concurrent_downloads = 10;
|
||||
|
||||
// Example variables for simple project case
|
||||
let name = "Example".to_string();
|
||||
let game_version = "1.19.2".to_string();
|
||||
let modloader = ModLoader::Vanilla;
|
||||
let loader_version = "stable".to_string();
|
||||
|
||||
// let icon = Some(
|
||||
// Path::new("../icon_test.png")
|
||||
// .canonicalize()
|
||||
// .expect("Icon could be not be found. If not using, set to None"),
|
||||
// );
|
||||
let icon = None;
|
||||
|
||||
// Clear profiles
|
||||
println!("Clearing profiles.");
|
||||
let h = profile::list().await?;
|
||||
for (path, _) in h.into_iter() {
|
||||
profile::remove(&path).await?;
|
||||
}
|
||||
|
||||
println!("Creating/adding profile.");
|
||||
// Attempt to create a profile. If that fails, try adding one from the same path.
|
||||
// TODO: actually do a nested error check for the correct profile error.
|
||||
let profile_path = profile_create(
|
||||
name.clone(),
|
||||
game_version,
|
||||
modloader,
|
||||
loader_version,
|
||||
icon,
|
||||
)
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
// async closure for testing any desired edits
|
||||
// (ie: changing the java runtime of an added profile)
|
||||
println!("Editing.");
|
||||
profile::edit(&profile_path, |profile| {
|
||||
// Eg: Java. TODO: hook up to jre.rs class to pick optimal java
|
||||
profile.java = Some(JavaSettings {
|
||||
install: Some(Path::new("/usr/bin/java").to_path_buf()),
|
||||
extra_arguments: None,
|
||||
});
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
println!("Authenticating.");
|
||||
// Attempt to create credentials and run.
|
||||
let proc_lock = match authenticate_run().await {
|
||||
Ok(credentials) => {
|
||||
println!("Running.");
|
||||
profile::run(&canonicalize(&profile_path)?, &credentials).await
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Could not authenticate: {}.\nAttempting stored authentication.",e);
|
||||
// Attempt to load credentials if Hydra is down/rate limit hit
|
||||
let users = auth::users().await.unwrap();
|
||||
let credentials = users.first().unwrap();
|
||||
|
||||
println!("Running.");
|
||||
profile::run(&canonicalize(&profile_path)?, credentials).await
|
||||
}
|
||||
}?;
|
||||
|
||||
println!("Started. Waiting...");
|
||||
let mut proc: RwLockWriteGuard<Child> = proc_lock.write().await;
|
||||
profile::wait_for(&mut proc).await?;
|
||||
|
||||
// Run MC
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user