diff --git a/Cargo.lock b/Cargo.lock index 91c2ce933..3127c00cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,7 +575,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid", + "uuid 1.4.0", ] [[package]] @@ -986,7 +986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "serde", - "uuid", + "uuid 1.4.0", ] [[package]] @@ -1107,6 +1107,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "discord-rich-presence" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fc4beffb85ee1461588499073a4d9c20dcc7728c4b13d6b282ab6c508947e5" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "uuid 0.8.2", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -2522,7 +2534,7 @@ dependencies = [ "crash-handler", "minidumper", "thiserror", - "uuid", + "uuid 1.4.0", ] [[package]] @@ -3810,7 +3822,7 @@ dependencies = [ "thiserror", "time 0.3.22", "url", - "uuid", + "uuid 1.4.0", ] [[package]] @@ -4299,7 +4311,7 @@ dependencies = [ "serde", "tao-macros", "unicode-segmentation", - "uuid", + "uuid 1.4.0", "windows 0.39.0", "windows-implement", "x11-dl", @@ -4382,7 +4394,7 @@ dependencies = [ "time 0.3.22", "tokio", "url", - "uuid", + "uuid 1.4.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -4428,7 +4440,7 @@ dependencies = [ "tauri-utils", "thiserror", "time 0.3.22", - "uuid", + "uuid 1.4.0", "walkdir", ] @@ -4506,7 +4518,7 @@ dependencies = [ "tauri-utils", "thiserror", "url", - "uuid", + "uuid 1.4.0", "webview2-com", "windows 0.39.0", ] @@ -4524,7 +4536,7 @@ dependencies = [ "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid", + "uuid 1.4.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -4606,6 +4618,7 @@ dependencies = [ "chrono", "daedalus", "dirs 5.0.1", + "discord-rich-presence", "dunce", "futures", "indicatif", @@ -4633,7 +4646,7 @@ dependencies = [ "tracing-error 0.1.2", "tracing-subscriber 0.2.25", "url", - "uuid", + "uuid 1.4.0", "whoami", "winreg 0.50.0", "zip", @@ -4661,7 +4674,7 @@ dependencies = [ "tracing-futures", "tracing-subscriber 0.3.17", "url", - "uuid", + "uuid 1.4.0", "webbrowser", "winreg 0.11.0", ] @@ -4695,7 +4708,7 @@ dependencies = [ "tracing", "tracing-error 0.1.2", "url", - "uuid", + "uuid 1.4.0", "window-shadows", ] @@ -4725,7 +4738,7 @@ dependencies = [ "tracing-error 0.1.2", "tracing-subscriber 0.2.25", "url", - "uuid", + "uuid 1.4.0", "webbrowser", ] @@ -5209,6 +5222,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.10", +] + [[package]] name = "uuid" version = "1.4.0" diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index a839d531d..45cfa41f6 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -55,6 +55,8 @@ dunce = "1.0.3" whoami = "1.4.0" +discord-rich-presence = "0.2.3" + [target.'cfg(windows)'.dependencies] winreg = "0.50.0" diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index 4bc94b5e1..2c6efeb9b 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -27,7 +27,7 @@ pub mod prelude { jre, metadata, pack, process, profile::{self, create, Profile}, settings, - state::JavaGlobals, + state::{JavaGlobals, SetFullscreen}, state::{ProfilePathId, ProjectPathId}, util::{ io::{canonicalize, IOError}, diff --git a/theseus/src/api/pack/import/atlauncher.rs b/theseus/src/api/pack/import/atlauncher.rs index 2edaa8882..a162e5620 100644 --- a/theseus/src/api/pack/import/atlauncher.rs +++ b/theseus/src/api/pack/import/atlauncher.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, path::PathBuf}; use serde::{Deserialize, Serialize}; -use tokio::fs; use crate::{ event::LoadingBarId, @@ -106,7 +105,7 @@ pub struct ATLauncherMod { // Check if folder has a instance.json that parses pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool { let instance: String = - fs::read_to_string(&instance_folder.join("instance.json")) + io::read_to_string(&instance_folder.join("instance.json")) .await .unwrap_or("".to_string()); let instance: Result = diff --git a/theseus/src/api/pack/import/curseforge.rs b/theseus/src/api/pack/import/curseforge.rs index 4387646be..485ae6587 100644 --- a/theseus/src/api/pack/import/curseforge.rs +++ b/theseus/src/api/pack/import/curseforge.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use tokio::fs; use crate::{ prelude::{ModLoader, ProfilePathId}, @@ -42,7 +41,7 @@ pub struct MinecraftInstance { // Check if folder has a minecraftinstance.json that parses pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool { let minecraftinstance: String = - fs::read_to_string(&instance_folder.join("minecraftinstance.json")) + io::read_to_string(&instance_folder.join("minecraftinstance.json")) .await .unwrap_or("".to_string()); let minecraftinstance: Result = diff --git a/theseus/src/api/pack/import/gdlauncher.rs b/theseus/src/api/pack/import/gdlauncher.rs index 6b642cb27..2d9c86e18 100644 --- a/theseus/src/api/pack/import/gdlauncher.rs +++ b/theseus/src/api/pack/import/gdlauncher.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use tokio::fs; use crate::{ prelude::{ModLoader, ProfilePathId}, @@ -32,7 +31,7 @@ pub struct GDLauncherLoader { // Check if folder has a config.json that parses pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool { let config: String = - fs::read_to_string(&instance_folder.join("config.json")) + io::read_to_string(&instance_folder.join("config.json")) .await .unwrap_or("".to_string()); let config: Result = diff --git a/theseus/src/api/pack/import/mmc.rs b/theseus/src/api/pack/import/mmc.rs index d62cb6c9d..f4e00b22a 100644 --- a/theseus/src/api/pack/import/mmc.rs +++ b/theseus/src/api/pack/import/mmc.rs @@ -1,7 +1,6 @@ use std::path::{Path, PathBuf}; use serde::{de, Deserialize, Serialize}; -use tokio::fs; use crate::{ pack::{ @@ -126,7 +125,7 @@ pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool { let instance_cfg = instance_folder.join("instance.cfg"); let mmc_pack = instance_folder.join("mmc-pack.json"); - let mmc_pack = match fs::read_to_string(&mmc_pack).await { + let mmc_pack = match io::read_to_string(&mmc_pack).await { Ok(mmc_pack) => mmc_pack, Err(_) => return false, }; diff --git a/theseus/src/api/pack/import/mod.rs b/theseus/src/api/pack/import/mod.rs index bc1e908c5..f5d026e00 100644 --- a/theseus/src/api/pack/import/mod.rs +++ b/theseus/src/api/pack/import/mod.rs @@ -66,45 +66,46 @@ pub async fn get_importable_instances( } // Import an instance from a launcher type and base path +// Note: this *deletes* the submitted empty profile #[theseus_macros::debug_pin] #[tracing::instrument] pub async fn import_instance( - profile_path: ProfilePathId, + profile_path: ProfilePathId, // This should be a blank profile launcher_type: ImportLauncherType, base_path: PathBuf, instance_folder: String, ) -> crate::Result<()> { tracing::debug!("Importing instance from {instance_folder}"); - match launcher_type { + let res = match launcher_type { ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => { mmc::import_mmc( - base_path, // path to base mmc folder - instance_folder, // instance folder in mmc_base_path - profile_path, // path to profile + base_path, // path to base mmc folder + instance_folder, // instance folder in mmc_base_path + profile_path.clone(), // path to profile ) - .await?; + .await } ImportLauncherType::ATLauncher => { atlauncher::import_atlauncher( - base_path, // path to atlauncher folder - instance_folder, // instance folder in atlauncher - profile_path, // path to profile + base_path, // path to atlauncher folder + instance_folder, // instance folder in atlauncher + profile_path.clone(), // path to profile ) - .await?; + .await } ImportLauncherType::GDLauncher => { gdlauncher::import_gdlauncher( base_path.join("instances").join(instance_folder), // path to gdlauncher folder - profile_path, // path to profile + profile_path.clone(), // path to profile ) - .await?; + .await } ImportLauncherType::Curseforge => { curseforge::import_curseforge( base_path.join("Instances").join(instance_folder), // path to curseforge folder - profile_path, // path to profile + profile_path.clone(), // path to profile ) - .await?; + .await } ImportLauncherType::Unknown => { return Err(crate::ErrorKind::InputError( @@ -112,6 +113,16 @@ pub async fn import_instance( ) .into()); } + }; + + // If import failed, delete the profile + match res { + Ok(_) => {} + Err(e) => { + tracing::warn!("Import failed: {:?}", e); + let _ = crate::api::profile::remove(&profile_path).await; + return Err(e); + } } // Check existing managed packs for potential updates diff --git a/theseus/src/api/process.rs b/theseus/src/api/process.rs index c9a86293d..c9ae281ba 100644 --- a/theseus/src/api/process.rs +++ b/theseus/src/api/process.rs @@ -121,7 +121,7 @@ pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> { } } -// Kill a running child process directly, and wait for it to be killed +// Kill a running child process directly #[tracing::instrument(skip(running))] pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> { running @@ -131,7 +131,7 @@ pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> { .kill() .await .map_err(IOError::from)?; - wait_for(running).await + Ok(()) } // Await on the completion of a child process directly diff --git a/theseus/src/api/profile/mod.rs b/theseus/src/api/profile/mod.rs index 41f1c97eb..a585d80d7 100644 --- a/theseus/src/api/profile/mod.rs +++ b/theseus/src/api/profile/mod.rs @@ -7,7 +7,9 @@ use crate::event::LoadingBarType; use crate::pack::install_from::{ EnvType, PackDependency, PackFile, PackFileHash, PackFormat, }; -use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; +use crate::prelude::{ + JavaVersion, ProfilePathId, ProjectPathId, SetFullscreen, +}; use crate::state::ProjectMetadata; use crate::util::io::{self, IOError}; @@ -838,9 +840,23 @@ pub async fn run_credentials( None }; + // Any options.txt settings that we want set, add here + let mut mc_set_options: Vec<(String, String)> = Vec::new(); + match profile.force_fullscreen { + SetFullscreen::LeaveUnset => {} + SetFullscreen::SetWindowed => { + mc_set_options + .push(("fullscreen".to_string(), "false".to_string())); + } + SetFullscreen::SetFullscreen => { + mc_set_options.push(("fullscreen".to_string(), "true".to_string())); + } + } + let mc_process = crate::launcher::launch_minecraft( java_args, env_args, + &mc_set_options, wrapper, &memory, &resolution, diff --git a/theseus/src/error.rs b/theseus/src/error.rs index 0caee272a..d036a1051 100644 --- a/theseus/src/error.rs +++ b/theseus/src/error.rs @@ -49,6 +49,9 @@ pub enum ErrorKind { #[error("Incorrect Sha1 hash for download: {0} != {1}")] HashError(String, String), + #[error("Regex error: {0}")] + RegexError(#[from] regex::Error), + #[error("Paths stored in the database need to be valid UTF-8: {0}")] UTFError(std::path::PathBuf), diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 89ec1a70a..07b9043e2 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -2,6 +2,7 @@ use crate::event::emit::{emit_loading, init_or_edit_loading}; use crate::event::{LoadingBarId, LoadingBarType}; use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY}; +use crate::launcher::io::IOError; use crate::prelude::JavaVersion; use crate::state::ProfileInstallStage; use crate::util::io; @@ -164,6 +165,16 @@ pub async fn install_minecraft( ) })?; + // Test jre version + let java_version = jre::check_jre(java_version.path.clone().into()) + .await? + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Java path invalid or non-functional: {}", + java_version.path + )) + })?; + // Download minecraft (5-90) download::download_minecraft( &state, @@ -246,6 +257,7 @@ pub async fn install_minecraft( )?) .output() .await + .map_err(|e| IOError::with_path(e, &java_version.path)) .map_err(|err| { crate::ErrorKind::LauncherError(format!( "Error running processor: {err}", @@ -291,6 +303,7 @@ pub async fn install_minecraft( pub async fn launch_minecraft( java_args: &[String], env_args: &[(String, String)], + mc_set_options: &[(String, String)], wrapper: &Option, memory: &st::MemorySettings, resolution: &st::WindowSize, @@ -440,6 +453,33 @@ pub async fn launch_minecraft( } command.envs(env_args); + // Overwrites the minecraft options.txt file with the settings from the profile + // Uses 'a:b' syntax which is not quite yaml + use regex::Regex; + + let options_path = instance_path.join("options.txt"); + let mut options_string = String::new(); + + if options_path.exists() { + options_string = io::read_to_string(&options_path).await?; + } + + for (key, value) in mc_set_options { + let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; + // check if the regex exists in the file + if !re.is_match(&options_string) { + // The key was not found in the file, so append it + options_string.push_str(&format!("\n{}:{}", key, value)); + } else { + let replaced_string = re + .replace_all(&options_string, &format!("{}:{}", key, value)) + .to_string(); + options_string = replaced_string; + } + } + + io::write(&options_path, options_string).await?; + // Get Modrinth logs directories let datetime_string = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); @@ -501,6 +541,14 @@ pub async fn launch_minecraft( } } + { + // Add game played to discord rich presence + let _ = state + .discord_rpc + .set_activity(&format!("Playing {}", profile.metadata.name), true) + .await; + } + // Create Minecraft child by inserting it into the state // This also spawns the process and prepares the subsequent processes let mut state_children = state.children.write().await; diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index c68fe2997..830f894b1 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -145,6 +145,13 @@ impl Children { } } + { + // Clear game played for Discord RPC + // May have other active processes, so we clear to the next running process + let state = crate::State::get().await?; + let _ = state.discord_rpc.clear_to_default(true).await; + } + // If in tauri, window should show itself again after process exists if it was hidden #[cfg(feature = "tauri")] { diff --git a/theseus/src/state/discord.rs b/theseus/src/state/discord.rs new file mode 100644 index 000000000..2adb4bb7d --- /dev/null +++ b/theseus/src/state/discord.rs @@ -0,0 +1,167 @@ +use std::sync::{atomic::AtomicBool, Arc}; + +use discord_rich_presence::{ + activity::{Activity, Assets}, + DiscordIpc, DiscordIpcClient, +}; +use tokio::sync::RwLock; + +use crate::State; + +pub struct DiscordGuard { + client: Arc>, + connected: Arc, +} + +impl DiscordGuard { + /// Initialize discord IPC client, and attempt to connect to it + /// If it fails, it will still return a DiscordGuard, but the client will be unconnected + pub async fn init() -> crate::Result { + let mut dipc = + DiscordIpcClient::new("1084015525241311292").map_err(|e| { + crate::ErrorKind::OtherError(format!( + "Could not create Discord client {}", + e, + )) + })?; + let res = dipc.connect(); // Do not need to connect to Discord to use app + let connected = if res.is_ok() { + Arc::new(AtomicBool::new(true)) + } else { + Arc::new(AtomicBool::new(false)) + }; + + let client = Arc::new(RwLock::new(dipc)); + Ok(DiscordGuard { client, connected }) + } + + /// If the client failed connecting during init(), this will check for connection and attempt to reconnect + /// This MUST be called first in any client method that requires a connection, because those can PANIC if the client is not connected + /// (No connection is different than a failed connection, the latter will not panic and can be retried) + pub async fn retry_if_not_ready(&self) -> bool { + let mut client = self.client.write().await; + if !self.connected.load(std::sync::atomic::Ordering::Relaxed) { + if client.connect().is_ok() { + self.connected + .store(true, std::sync::atomic::Ordering::Relaxed); + return true; + } + return false; + } + true + } + + /// Set the activity to the given message + pub async fn set_activity( + &self, + msg: &str, + reconnect_if_fail: bool, + ) -> crate::Result<()> { + // Attempt to connect if not connected. Do not continue if it fails, as the client.set_activity can panic if it never was connected + if !self.retry_if_not_ready().await { + return Ok(()); + } + + let activity = Activity::new().state(msg).assets( + Assets::new() + .large_image("modrinth_simple") + .large_text("Modrinth Logo"), + ); + + // Attempt to set the activity + // If the existing connection fails, attempt to reconnect and try again + let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> = + self.client.write().await; + let res = client.set_activity(activity.clone()); + let could_not_set_err = |e: Box| { + crate::ErrorKind::OtherError(format!( + "Could not update Discord activity {}", + e, + )) + }; + + if reconnect_if_fail { + if let Err(_e) = res { + client.reconnect().map_err(|e| { + crate::ErrorKind::OtherError(format!( + "Could not reconnect to Discord IPC {}", + e, + )) + })?; + return Ok(client + .set_activity(activity) + .map_err(could_not_set_err)?); // try again, but don't reconnect if it fails again + } + } else { + res.map_err(could_not_set_err)?; + } + + Ok(()) + } + + /// Clear the activity + pub async fn clear_activity( + &self, + reconnect_if_fail: bool, + ) -> crate::Result<()> { + // Attempt to connect if not connected. Do not continue if it fails, as the client.clear_activity can panic if it never was connected + if !self.retry_if_not_ready().await { + return Ok(()); + } + + // Attempt to clear the activity + // If the existing connection fails, attempt to reconnect and try again + let mut client = self.client.write().await; + let res = client.clear_activity(); + + let could_not_clear_err = |e: Box| { + crate::ErrorKind::OtherError(format!( + "Could not clear Discord activity {}", + e, + )) + }; + + if reconnect_if_fail { + if res.is_err() { + client.reconnect().map_err(|e| { + crate::ErrorKind::OtherError(format!( + "Could not reconnect to Discord IPC {}", + e, + )) + })?; + return Ok(client + .clear_activity() + .map_err(could_not_clear_err)?); // try again, but don't reconnect if it fails again + } + } else { + res.map_err(could_not_clear_err)?; + } + Ok(()) + } + + /// Clear the activity, but if there is a running profile, set the activity to that instead + pub async fn clear_to_default( + &self, + reconnect_if_fail: bool, + ) -> crate::Result<()> { + let state: Arc> = + State::get().await?; + if let Some(existing_child) = state + .children + .read() + .await + .running_profile_paths() + .await? + .first() + { + self.set_activity( + &format!("Playing {}", existing_child), + reconnect_if_fail, + ) + .await?; + } else { + self.clear_activity(reconnect_if_fail).await?; + } + Ok(()) + } +} diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index c16a1d9db..0f8be0965 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -48,6 +48,9 @@ pub use self::java_globals::*; mod safe_processes; pub use self::safe_processes::*; +mod discord; +pub use self::discord::*; + // Global state // RwLock on state only has concurrent reads, except for config dir change which takes control of the State static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); @@ -81,6 +84,9 @@ pub struct State { /// Launcher processes that should be safely exited on shutdown pub(crate) safety_processes: RwLock, + /// Discord RPC + pub discord_rpc: DiscordGuard, + /// File watcher debouncer pub(crate) file_watcher: RwLock>, } @@ -156,6 +162,9 @@ impl State { let children = Children::new(); let auth_flow = AuthTask::new(); let safety_processes = SafeProcesses::new(); + + let discord_rpc = DiscordGuard::init().await?; + emit_loading(&loading_bar, 10.0, None).await?; Ok::, crate::Error>(RwLock::new(Self { @@ -175,6 +184,7 @@ impl State { children: RwLock::new(children), auth_flow: RwLock::new(auth_flow), tags: RwLock::new(tags), + discord_rpc, safety_processes: RwLock::new(safety_processes), file_watcher: RwLock::new(file_watcher), })) diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index bb1b14344..edfc1c2c9 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -149,6 +149,8 @@ pub struct Profile { pub memory: Option, #[serde(skip_serializing_if = "Option::is_none")] pub resolution: Option, + #[serde(default)] + pub force_fullscreen: SetFullscreen, #[serde(skip_serializing_if = "Option::is_none")] pub hooks: Option, pub projects: HashMap, @@ -223,6 +225,21 @@ impl ModLoader { } } +#[derive(Serialize, Deserialize, Clone, Debug, Copy)] +pub enum SetFullscreen { + #[serde(rename = "Leave unset")] + LeaveUnset, + #[serde(rename = "Set windowed")] + SetWindowed, + #[serde(rename = "Set fullscreen")] + SetFullscreen, +} +impl Default for SetFullscreen { + fn default() -> Self { + Self::LeaveUnset + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct JavaSettings { #[serde(skip_serializing_if = "Option::is_none")] @@ -268,6 +285,7 @@ impl Profile { java: None, memory: None, resolution: None, + force_fullscreen: SetFullscreen::LeaveUnset, hooks: None, modrinth_update_version: None, }) diff --git a/theseus/src/util/jre.rs b/theseus/src/util/jre.rs index 46245e5d2..07a3454e1 100644 --- a/theseus/src/util/jre.rs +++ b/theseus/src/util/jre.rs @@ -46,11 +46,9 @@ pub async fn get_all_jre() -> Result, JREError> { ]; for java_path in java_paths { let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue }; - for java_subpath in java_subpaths { - if let Ok(java_subpath) = java_subpath { - let path = java_subpath.path(); - jre_paths.insert(path.join("bin")); - } + for java_subpath in java_subpaths.flatten() { + let path = java_subpath.path(); + jre_paths.insert(path.join("bin")); } } @@ -93,19 +91,17 @@ pub async fn get_all_jre() -> Result, JREError> { pub fn get_paths_from_jre_winregkey(jre_key: RegKey) -> HashSet { let mut jre_paths = HashSet::new(); - for subkey in jre_key.enum_keys() { - if let Ok(subkey) = subkey { - if let Ok(subkey) = jre_key.open_subkey(subkey) { - let subkey_value_names = - [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"]; + for subkey in jre_key.enum_keys().flatten() { + if let Ok(subkey) = jre_key.open_subkey(subkey) { + let subkey_value_names = + [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"]; - for subkey_value in subkey_value_names { - let path: Result = - subkey.get_value(subkey_value); - let Ok(path) = path else {continue}; + for subkey_value in subkey_value_names { + let path: Result = + subkey.get_value(subkey_value); + let Ok(path) = path else {continue}; - jre_paths.insert(PathBuf::from(path).join("bin")); - } + jre_paths.insert(PathBuf::from(path).join("bin")); } } } diff --git a/theseus_gui/src-tauri/src/api/import.rs b/theseus_gui/src-tauri/src/api/import.rs index f30e77d0a..de7e075ee 100644 --- a/theseus_gui/src-tauri/src/api/import.rs +++ b/theseus_gui/src-tauri/src/api/import.rs @@ -29,6 +29,7 @@ pub async fn import_get_importable_instances( } /// Import an instance from a launcher type and base path +/// profile_path should be a blank profile for this purpose- if the function fails, it will be deleted /// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1") #[tauri::command] pub async fn import_import_instance( diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index d83a88815..99f1a6e2d 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -276,6 +276,7 @@ pub struct EditProfile { pub memory: Option, pub resolution: Option, pub hooks: Option, + pub force_fullscreen: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -315,6 +316,9 @@ pub async fn profile_edit( prof.java = edit_profile.java.clone(); prof.memory = edit_profile.memory; prof.resolution = edit_profile.resolution; + if let Some(force_fullscreen) = edit_profile.force_fullscreen { + prof.force_fullscreen = force_fullscreen; + } prof.hooks = edit_profile.hooks.clone(); prof.metadata.date_modified = chrono::Utc::now(); diff --git a/theseus_gui/src/pages/instance/Options.vue b/theseus_gui/src/pages/instance/Options.vue index 59f80124e..a896e7425 100644 --- a/theseus_gui/src/pages/instance/Options.vue +++ b/theseus_gui/src/pages/instance/Options.vue @@ -182,6 +182,9 @@ Window +
+ +
@@ -439,10 +442,12 @@ const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024) const overrideWindowSettings = ref(!!props.instance.resolution) const resolution = ref(props.instance.resolution ?? globalSettings.game_resolution) - const overrideHooks = ref(!!props.instance.hooks) const hooks = ref(props.instance.hooks ?? globalSettings.hooks) +const fullscreenOptions = ref(['Leave unchanged', 'Set windowed', 'Set fullscreen']) +const forceFullscreen = ref(props.instance.force_fullscreen) + watch( [ title, @@ -458,6 +463,7 @@ watch( memory, overrideWindowSettings, resolution, + forceFullscreen, overrideHooks, hooks, ], @@ -505,6 +511,10 @@ watch( editProfile.resolution = resolution.value } + if (forceFullscreen.value) { + editProfile.force_fullscreen = forceFullscreen.value + } + if (overrideHooks.value) { editProfile.hooks = hooks.value }