diff --git a/Cargo.lock b/Cargo.lock index 447ce1ca..fc728e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,6 +3070,7 @@ dependencies = [ "thiserror 1.0.64", "tokio", "tokio-stream", + "tokio-util", "tracing", "tracing-appender", "tracing-error", diff --git a/Cargo.toml b/Cargo.toml index 344305ec..607a21e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,7 @@ webbrowser = { version = "1.0" } # io tokio = { version = "1.40", features = [ "full" ] } +tokio-util = { version = "0.7", features = [ "compat" ] } tokio-stream = { version = "0.1.16", features = [ "fs" ] } notify = { version = "6.1", default-features = false } notify-debouncer-mini = { version = "0.4", default-features = false } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index fb736f05..8ca91d72 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -14,7 +14,7 @@ authors = { workspace = true } path = "src/lib.rs" [features] -default = [] +default = [ "cli" ] tauri = [ "dep:tauri", "dep:tauri-specta", @@ -51,6 +51,7 @@ serde_with = { workspace = true } # utils sea-orm = { workspace = true } tokio = { workspace = true } +tokio-util = { workspace = true } tokio-stream = { workspace = true } futures = { workspace = true } futures-lite = { workspace = true } diff --git a/packages/core/examples/cluster.rs b/packages/core/examples/cluster.rs index 586eeea5..2adab2d6 100644 --- a/packages/core/examples/cluster.rs +++ b/packages/core/examples/cluster.rs @@ -23,7 +23,7 @@ async fn main() -> LauncherResult<()> { println!("cluster created: {cluster:#?}"); println!("preparing cluster for launch"); - api::cluster::prepare_cluster(cluster).await?; + api::cluster::prepare_cluster(cluster, None).await?; Ok(()) } \ No newline at end of file diff --git a/packages/core/examples/launch.rs b/packages/core/examples/launch.rs new file mode 100644 index 00000000..d3d23f82 --- /dev/null +++ b/packages/core/examples/launch.rs @@ -0,0 +1,80 @@ +use chrono::{Days, Utc}; +use onelauncher_core::api::cluster::dao::update_cluster; +use onelauncher_core::api::proxy::ProxyDynamic; +use onelauncher_core::api::{self, setting_profiles}; +use onelauncher_core::error::LauncherResult; +use onelauncher_core::initialize_core; +use onelauncher_core::store::credentials::MinecraftCredentials; +use onelauncher_core::store::{CoreOptions, Dirs, State}; +use onelauncher_entity::loader::GameLoader; +use sea_orm::ActiveValue::Set; + +#[tokio::main] +pub async fn main() -> LauncherResult<()> { + initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?; + + let state = State::get().await?; + let dirs = Dirs::get().await?; + + // We get the default credentials (or for the sake of this example, return an empty player profile) + println!("Getting credentials..."); + let mut cred_store = state.credentials.write().await; + let creds = cred_store + .get_default() + .await? + .unwrap_or_else(|| MinecraftCredentials { + access_token: String::new(), + refresh_token: String::new(), + username: "Player1".to_string(), + id: uuid::Uuid::nil(), + expires: Utc::now().checked_add_days(Days::new(7)).unwrap(), + }); + + println!("Credentials: {creds:?}"); + drop(cred_store); + + // We create a settings profile so that we can run hooks + let profile_name = "profile with hooks example"; + if setting_profiles::dao::get_profile_by_name(profile_name).await?.is_none() { + setting_profiles::create_profile(profile_name, async |mut profile| { + profile.hook_pre = Set(Some("echo this is a pre-hook!".to_string())); + profile.hook_post = Set(Some("echo this is my post-hook!".to_string())); + + #[cfg(target_os = "linux")] + { + profile.hook_wrapper = Set(Some("mangohud".to_string())); + } + + Ok(profile) + }).await?; + } + + // Here we create the cluster + let cluster_name = "Launchable"; + let path = dirs.clusters_dir().join(cluster_name); + + let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_path(&path).await? { + cluster + } else { + api::cluster::create_cluster(cluster_name, "1.20.4", GameLoader::Fabric, None, None).await? + }; + + // We set the clusters + update_cluster(&mut cluster, async |mut cluster| { + cluster.setting_profile_name = Set(Some(profile_name.to_string())); + + Ok(cluster) + }).await?; + + println!("Using cluster: {} at '{}'", cluster.name, cluster.path); + + // Launch the game + let process = api::game::launch::launch_minecraft(&mut cluster, creds, None).await?; + println!("Process: {:?}", process.read().await); + + // We wait for the process to finish so that we can run code after + process.read().await.child_type.write().await.wait().await?; + println!("Successfully exited !"); + + Ok(()) +} diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs index 0681acb0..af494462 100644 --- a/packages/core/src/api/cluster/dao.rs +++ b/packages/core/src/api/cluster/dao.rs @@ -5,6 +5,8 @@ use sea_orm::{ActiveValue::Set, IntoActiveModel, prelude::*}; use crate::{error::{DaoError, LauncherResult}, store::State}; +pub type ClusterId = i64; + /// Inserts a cluster in the database. pub async fn insert_cluster( name: &str, @@ -34,7 +36,7 @@ pub async fn insert_cluster( /// Updates an existing cluster in the database. pub async fn update_cluster_by_id( - id: u64, + id: ClusterId, block: B, ) -> LauncherResult where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult { @@ -66,7 +68,7 @@ where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult LauncherResult<()> { +pub async fn delete_cluster_by_id(id: ClusterId) -> LauncherResult<()> { let state = State::get().await?; let db = &state.db; @@ -82,7 +84,7 @@ pub async fn delete_cluster_by_id(id: u64) -> LauncherResult<()> { } /// Gets a cluster by its ID from the database. -pub async fn get_cluster_by_id(id: u64) -> LauncherResult> { +pub async fn get_cluster_by_id(id: ClusterId) -> LauncherResult> { let state = State::get().await?; let db = &state.db; diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs index f2f2f227..0549a560 100644 --- a/packages/core/src/api/cluster/mod.rs +++ b/packages/core/src/api/cluster/mod.rs @@ -1,4 +1,5 @@ use chrono::Utc; +use dao::ClusterId; use interpulse::api::minecraft::VersionInfo; use onelauncher_entity::cluster_stage::ClusterStage; use onelauncher_entity::{clusters, java_versions}; @@ -31,6 +32,8 @@ pub enum ClusterError { MissingJavaVersion, #[error("cluster is in downloading stage")] ClusterDownloading, + #[error("cluster is already running")] + ClusterAlreadyRunning, } #[must_use] @@ -102,17 +105,17 @@ pub async fn create_cluster( } /// Make a cluster playable (installs all necessary game files and dependencies required to play) -pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&clusters::Model> { +pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option) -> LauncherResult<&clusters::Model> { const INGRESS_TOTAL: f64 = 100.0; const INGRESS_TASKS: f64 = 4.0; const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS; tracing::debug!("preparing cluster {}", cluster.name); - if cluster.stage == ClusterStage::Ready { - tracing::info!("cluster is already ready"); - return Ok(cluster); - } + // if cluster.stage == ClusterStage::Ready { + // tracing::info!("cluster is already ready"); + // return Ok(cluster); + // } let ingress_id = init_ingress( IngressType::PrepareCluster { @@ -144,7 +147,10 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&c .versions .iter() .find(|v| v.id == cluster.mc_version) - .ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?; + .ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))? + .clone(); + + drop(metadata); let loader_version = metadata::get_loader_version( &cluster.mc_version, @@ -155,10 +161,10 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&c // TASK 2 let mut version_info = metadata::download_version_info( - version, + &version, loader_version.as_ref(), Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), - None, + force, ) .await?; @@ -173,7 +179,7 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&c }; // TASK 3 - download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), None).await?; + download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?; // TASK 4 run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?; @@ -303,7 +309,7 @@ async fn run_forge_processors( Ok(()) } -pub async fn update_playtime(id: u64, duration: i64) -> LauncherResult { +pub async fn update_playtime(id: ClusterId, duration: i64) -> LauncherResult { dao::update_cluster_by_id(id, async |mut cluster| { let overall_played = cluster.overall_played.take().unwrap_or_default().unwrap_or_default(); diff --git a/packages/core/src/api/cluster/sync.rs b/packages/core/src/api/cluster/sync.rs index 7587e4b9..d5da5cdb 100644 --- a/packages/core/src/api/cluster/sync.rs +++ b/packages/core/src/api/cluster/sync.rs @@ -5,7 +5,7 @@ use serde::Serialize; use crate::{error::{DaoError, LauncherResult}, send_error, store::Dirs, utils::io}; -use super::dao; +use super::dao::{self, ClusterId}; #[onelauncher_macro::specta] #[derive(Debug, Clone, Serialize, PartialEq, Eq)] @@ -26,7 +26,7 @@ impl SyncAction { /// Syncs all clusters in the database. Returns a list of cluster IDs that are missing. #[tracing::instrument] -pub async fn sync_clusters() -> LauncherResult> { +pub async fn sync_clusters() -> LauncherResult> { let clusters = dao::get_all_clusters().await?; let mut missing_ids = Vec::new(); @@ -92,7 +92,7 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult LauncherResult { +pub async fn sync_cluster_by_id(id: ClusterId) -> LauncherResult { let cluster = dao::get_cluster_by_id(id) .await? .ok_or(DaoError::NotFound)?; diff --git a/packages/core/src/api/game/arguments.rs b/packages/core/src/api/game/arguments.rs index 6049ad5d..8122eed7 100644 --- a/packages/core/src/api/game/arguments.rs +++ b/packages/core/src/api/game/arguments.rs @@ -7,9 +7,11 @@ use futures::AsyncReadExt; use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType}; use interpulse::api::modded::SidedDataEntry; use interpulse::utils::get_path_from_artifact; +use onelauncher_entity::resolution::Resolution; use crate::constants::{self, DUMMY_REPLACE_NEWLINE}; use crate::error::LauncherResult; +use crate::store::credentials::MinecraftCredentials; use crate::store::Core; use crate::utils::io; @@ -20,9 +22,8 @@ pub fn java_arguments( libraries_path: &Path, classpaths: &str, version: &str, - mem_min: usize, - mem_max: usize, - custom_args: Vec, + mem_max: u32, + custom_args: String, java_arch: &str, ) -> LauncherResult> { let mut parsed = Vec::new(); @@ -56,75 +57,72 @@ pub fn java_arguments( } parsed.push(format!("-Xmx{mem_max}M")); - parsed.push(format!("-Xms{mem_min}M")); - for arg in custom_args { - if !arg.is_empty() { - parsed.push(arg); - } + if !custom_args.is_empty() { + parsed.push(custom_args); } Ok(parsed) } -// #[allow(clippy::too_many_arguments)] -// pub fn minecraft_arguments( -// args: Option<&[Argument]>, -// legacy_args: Option<&str>, -// creds: &MinecraftCredentials, -// version: &str, -// asset_index: &str, -// game_directory: &Path, -// assets_directory: &Path, -// version_type: VersionType, -// resolution: Resolution, -// java_arch: &str, -// ) -> LauncherResult> { -// if let Some(args) = args { -// let mut parsed = Vec::new(); -// parse_arguments( -// args, -// &mut parsed, -// |arg| { -// parse_minecraft_argument( -// arg, -// &creds.access_token, -// &creds.username, -// creds.id, -// version, -// asset_index, -// game_directory, -// assets_directory, -// version_type, -// resolution, -// ) -// }, -// java_arch, -// )?; +#[allow(clippy::too_many_arguments)] +pub fn minecraft_arguments( + args: Option<&[Argument]>, + legacy_args: Option<&str>, + creds: &MinecraftCredentials, + version: &str, + asset_index: &str, + game_directory: &Path, + assets_directory: &Path, + version_type: VersionType, + resolution: Resolution, + java_arch: &str, +) -> LauncherResult> { + if let Some(args) = args { + let mut parsed = Vec::new(); + parse_arguments( + args, + &mut parsed, + |arg| { + parse_minecraft_argument( + arg, + &creds.access_token, + &creds.username, + creds.id, + version, + asset_index, + game_directory, + assets_directory, + version_type, + resolution, + ) + }, + java_arch, + )?; -// Ok(parsed) -// } else if let Some(legacy_args) = legacy_args { -// let mut parsed = Vec::new(); -// for arg in legacy_args.split(' ') { -// parsed.push(parse_minecraft_argument( -// &arg.replace(' ', DUMMY_REPLACE_NEWLINE), -// &creds.access_token, -// &creds.username, -// creds.id, -// version, -// asset_index, -// game_directory, -// assets_directory, -// version_type, -// resolution, -// )?); -// } + Ok(parsed) + } else if let Some(legacy_args) = legacy_args { + let mut parsed = Vec::new(); + for arg in legacy_args.split(' ') { + parsed.push(parse_minecraft_argument( + &arg.replace(' ', DUMMY_REPLACE_NEWLINE), + &creds.access_token, + &creds.username, + creds.id, + version, + asset_index, + game_directory, + assets_directory, + version_type, + resolution, + )?); + } -// Ok(parsed) -// } else { -// Ok(Vec::new()) -// } -// } + Ok(parsed) + } else { + Ok(Vec::new()) + } +} pub fn processor_arguments, S: ::std::hash::BuildHasher>( libraries_path: &Path, @@ -206,7 +204,7 @@ pub fn parse_minecraft_argument( game_directory: &Path, assets_directory: &Path, version_type: VersionType, - resolution: (u32, u32), + resolution: Resolution, ) -> LauncherResult { #[allow(clippy::literal_string_with_formatting_args)] Ok(argument @@ -235,8 +233,8 @@ pub fn parse_minecraft_argument( &io::canonicalize(assets_directory)?.to_string_lossy(), ) .replace("${version_type}", version_type.as_str()) - .replace("${resolution_width}", &resolution.0.to_string()) - .replace("${resolution_height}", &resolution.1.to_string())) + .replace("${resolution_width}", &resolution.width.to_string()) + .replace("${resolution_height}", &resolution.height.to_string())) } #[allow(clippy::too_many_arguments)] diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index 771d8738..b1b146d1 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -1,22 +1,38 @@ +use std::path::PathBuf; use std::sync::Arc; -use onelauncher_entity::{cluster_stage::ClusterStage, prelude::model::*}; -use tokio::sync::RwLock; +use chrono::Utc; +use discord_rich_presence::activity::Timestamps; use merge::Merge; +use onelauncher_entity::prelude::model::*; +use sea_orm::ActiveValue::Set; +use tokio::process::Command; +use tokio::sync::RwLock; -use crate::{api::{cluster::{prepare_cluster, ClusterError}, game::metadata, java::{self, JavaError}, setting_profiles::{dao::get_profile_by_name, get_global_profile}}, error::LauncherResult, store::{credentials::MinecraftCredentials, processes::Process, State}, utils::io}; +use crate::api::cluster::dao::update_cluster; +use crate::api::cluster::{ClusterError, prepare_cluster}; +use crate::api::game::{arguments, log, metadata}; +use crate::api::java::{self, JavaError}; +use crate::api::setting_profiles::dao::get_profile_by_name; +use crate::api::setting_profiles::get_global_profile; +use crate::error::LauncherResult; +use crate::store::credentials::MinecraftCredentials; +use crate::store::processes::Process; +use crate::store::{Dirs, State}; +use crate::utils::io; -#[tracing::instrument] +#[tracing::instrument(skip_all)] pub async fn launch_minecraft( cluster: &mut Cluster, - creds: &MinecraftCredentials, + creds: MinecraftCredentials, + force: Option, ) -> LauncherResult>> { if cluster.stage.is_downloading() { return Err(ClusterError::ClusterDownloading.into()); - } else if cluster.stage == ClusterStage::NotReady { - prepare_cluster(cluster).await?; } + prepare_cluster(cluster, force).await?; + let mut settings = get_global_profile().await; if let Some(name) = &cluster.setting_profile_name { if let Some(profile) = get_profile_by_name(name).await? { @@ -25,45 +41,167 @@ pub async fn launch_minecraft( } let state = State::get().await?; - let instance_path = &io::canonicalize(&cluster.path)?; - let mut metadata = state.metadata.write().await; - let versions = &metadata.get_vanilla_or_fetch().await?.versions; + if !state.settings.read().await.allow_parallel_running_clusters { + let running = state.processes.has_running(cluster.id).await; + if running { + return Err(ClusterError::ClusterAlreadyRunning.into()); + } + } + + let cwd = &io::canonicalize(&cluster.path)?; + + let metadata = state.metadata.read().await; + let versions = &metadata.get_vanilla()?.versions; let version_index = versions .iter() .position(|it| it.id == cluster.mc_version) .ok_or_else(|| anyhow::anyhow!("invalid game version {}", cluster.mc_version))?; - - let version = &versions[version_index]; + let version = versions[version_index].clone(); let updated = version_index <= versions.iter().position(|x| x.id == "22w16a").unwrap_or(0); - let version_jar_name = cluster - .mc_loader_version - .as_ref() - .map_or(version.id.clone(), |it| { - format!("{}-{}", version.id.clone(), it.clone()) - }); - - let loader_version = metadata::get_loader_version(&cluster.mc_version, cluster.mc_loader, cluster.mc_loader_version.as_deref()).await?; - - let version_info = metadata::download_version_info( - version, - loader_version.as_ref(), - None, - None, + let loader_version = metadata::get_loader_version( + &cluster.mc_version, + cluster.mc_loader, + cluster.mc_loader_version.as_deref(), ) .await?; - drop(metadata); + let version_name = loader_version + .as_ref() + .map_or(version.id.clone(), |it| { + format!("{}-{}", version.id, it.id) + }); - let java = java::get_recommended_java(&version_info, Some(&settings)).await?; - if java.is_none() { + let version_info = + metadata::download_version_info(&version, loader_version.as_ref(), None, None).await?; + + let Some(java) = java::get_recommended_java(&version_info, Some(&settings)).await? else { return Err(JavaError::MissingJava.into()); + }; + + let java_path = PathBuf::from(&java.absolute_path); + java::check_java_runtime(&java_path, false).await?; + + let dirs = Dirs::get().await?; + let client_jar_path = dirs + .versions_dir() + .join(&version_name) + .join(format!("{version_name}.jar")); + + let mut command = if let Some(wrapper) = &settings.hook_wrapper { + let mut cmd = Command::new(wrapper); + cmd.arg(&java.absolute_path); + cmd + } else { + Command::new(&java.absolute_path) + }; + + let env_vars = settings.launch_env.iter().flat_map(|it| { + it.split(' ').map(|it| { + let mut split = it.split('='); + let key = split.next().unwrap_or_default(); + let value = split.next().unwrap_or_default(); + (key.to_string(), value.to_string()) + }) + }); + + #[cfg(target_os = "macos")] + if std::env::var("CARGO").is_ok() { + command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); } + command.env_remove("_JAVA_OPTIONS"); + command.envs(env_vars); + let args = version_info.arguments.clone().unwrap_or_default(); - todo!() -} \ No newline at end of file + command + .args( + arguments::java_arguments( + args.get(&interpulse::api::minecraft::ArgumentType::Jvm) + .map(Vec::as_slice), + &dirs.natives_dir().join(&version_name), + &dirs.libraries_dir(), + &arguments::classpaths( + &dirs.libraries_dir(), + version_info.libraries.as_slice(), + &client_jar_path, + &java.arch, + updated, + )?, + &version_name, + settings.mem_max.unwrap_or(2048), + settings.launch_args.clone().unwrap_or_default(), + &java.arch, + )? + .into_iter(), + ) + .arg(version_info.main_class.clone()) + .args( + arguments::minecraft_arguments( + args.get(&interpulse::api::minecraft::ArgumentType::Game) + .map(Vec::as_slice), + version_info.minecraft_arguments.as_deref(), + &creds, + &version.id, + &version_info.asset_index.id, + cwd, + &dirs.assets_dir(), + version.type_, + settings.res.unwrap_or_default(), + &java.arch, + )? + .into_iter(), + ) + .current_dir(cwd); + + // TODO: Make all options configurable? + let mc_options = &[( + String::from("fullscreen"), + settings.force_fullscreen.unwrap_or(false).to_string(), + )]; + + if !mc_options.is_empty() { + let options_path = cwd.join("options.txt"); + let mut options_string = if options_path.exists() { + io::read_to_string(&options_path).await? + } else { + String::new() + }; + for (key, value) in mc_options { + let re = regex::Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; + if re.is_match(&options_string) { + let replaced_string = re + .replace_all(&options_string, &format!("{key}:{value}")) + .to_string(); + options_string = replaced_string; + } else { + use std::fmt::Write; + let _ = write!(options_string, "\n{key}:{value}"); + } + } + + io::write(&options_path, options_string).await?; + } + + let start_time = Utc::now().timestamp(); + update_cluster(cluster, async |mut cluster| { + cluster.last_played = Set(Some(start_time)); + Ok(cluster) + }) + .await?; + + let censors = log::create_censors(&creds); + + if let Some(discord) = &state.rpc { + discord.set_message( + &format!("Playing {}", cluster.name), + Some(Timestamps::new().start(start_time)), + ).await; + } + + state.processes.spawn(cluster.id, cwd.clone(), creds, censors, &settings, command).await +} diff --git a/packages/core/src/api/game/log.rs b/packages/core/src/api/game/log.rs index c514a25d..7b26d635 100644 --- a/packages/core/src/api/game/log.rs +++ b/packages/core/src/api/game/log.rs @@ -1,27 +1,36 @@ -use std::sync::LazyLock; +use std::collections::HashMap; use crate::store::credentials::MinecraftCredentials; -static WHOAMI: LazyLock<(String, String)> = LazyLock::new(|| { - (whoami::username(), whoami::realname()) -}); +pub type CensorMap = HashMap; #[must_use] -pub fn censor_line( +pub fn create_censors( creds: &MinecraftCredentials, - mut output: String -) -> String { - let username = &WHOAMI.0; - let realname = &WHOAMI.1; +) -> CensorMap { + let mut map = HashMap::new(); - output = output.replace(&format!("/{username}/"), "/{ENV_USERNAME}/"); - output = output.replace(&format!("\\{username}\\"), "\\{ENV_USERNAME}\\"); - output = output.replace(&format!("/{realname}/"), "/{ENV_REALNAME}/"); - output = output.replace(&format!("\\{realname}\\"), "\\{ENV_REALNAME}\\"); - output = output.replace(&creds.access_token, "{MC_ACCESS_TOKEN}"); - output = output.replace(&creds.username, "{MC_USERNAME}"); - output = output.replace(&creds.id.as_simple().to_string(), "{MC_UUID}"); - output = output.replace(&creds.id.as_hyphenated().to_string(), "{MC_UUID}"); + let username = whoami::username(); + let realname = whoami::realname(); - output + map.insert(format!("/{username}/"), "/{ENV_USERNAME}/".to_string()); + map.insert(format!("\\{username}\\"), "\\{ENV_USERNAME}\\".to_string()); + map.insert(format!("/{realname}/"), "/{ENV_REALNAME}/".to_string()); + map.insert(format!("\\{realname}\\"), "\\{ENV_REALNAME}\\".to_string()); + + if !creds.access_token.is_empty() { + map.insert(creds.access_token.clone(), "{MC_ACCESS_TOKEN}".to_string()); + } + + map.insert(creds.username.clone(), "{MC_USERNAME}".to_string()); + map.insert(creds.id.as_simple().to_string(), "{MC_UUID}".to_string()); + map.insert(creds.id.as_hyphenated().to_string(), "{MC_UUID}".to_string()); + + map +} + +pub fn censor_line(censors: &CensorMap, line: &mut String) { + for (key, value) in censors { + *line = line.replace(key, value); + } } \ No newline at end of file diff --git a/packages/core/src/api/game/metadata.rs b/packages/core/src/api/game/metadata.rs index 6a02b652..f869e7ee 100644 --- a/packages/core/src/api/game/metadata.rs +++ b/packages/core/src/api/game/metadata.rs @@ -50,7 +50,7 @@ pub async fn download_minecraft( } // MARK: Version Info -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip(ingress, force))] pub async fn download_version_info( version: &Version, loader: Option<&LoaderVersion>, @@ -120,7 +120,7 @@ pub async fn download_assets_index( tracing::debug!("loading assets index for version {}", version.id); let path = Dirs::get_assets_index_dir() .await? - .join(version.asset_index.id.clone()); + .join(format!("{}.json", version.asset_index.id)); // let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0)); // let ingress_ref = ingress_ref.as_ref(); @@ -274,10 +274,10 @@ pub async fn download_libraries( tracing::debug!("loading libraries for version {}", version); let lib_dir = Dirs::get_libraries_dir().await?; - let natives_dir = Dirs::get_natives_dir().await?; + let natives_dest = Dirs::get_natives_dir().await?.join(version); io::create_dir_all(&lib_dir).await?; - io::create_dir_all(&natives_dir).await?; + io::create_dir_all(&natives_dest).await?; let num_files = libraries.len(); let ingress = ingress.ingress_sub(|total| total / num_files as f64); @@ -297,15 +297,16 @@ pub async fn download_libraries( return Ok(()); } + let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?; + let path = lib_dir.join(&artifact_path); + + if path.exists() && !force.unwrap_or(false) { + tracing::debug!("library {} is installed, skipping", &lib.name); + return Ok(()); + } + tokio::try_join! { async { - let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?; - let path = lib_dir.join(&artifact_path); - - if path.exists() && !force.unwrap_or(false) { - return Ok(()); - } - if let Some(interpulse::api::minecraft::LibraryDownloads { artifact: Some(ref artifact), .. }) = lib.downloads { @@ -343,6 +344,7 @@ pub async fn download_libraries( let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH); if let Some(native) = classifiers.get(&parsed) { tracing::trace!("found native library {}", &lib.name); + let data = http::fetch_advanced( Method::GET, &native.url, @@ -352,9 +354,12 @@ pub async fn download_libraries( ingress, ).await?; - let dest = natives_dir.join(version); - io::unzip_bytes(data.to_vec(), &dest).await?; - tracing::trace!("extracted native {} to path {:?}", &lib.name, &dest); + io::unzip_bytes_filtered( + data.to_vec(), + Some(|name: &str| !name.starts_with("META-INF")), + &natives_dest + ).await?; + tracing::trace!("extracted native {} to path {:?}", &lib.name, &natives_dest); } } @@ -380,6 +385,7 @@ pub async fn download_libraries( /// Gets the loader version for a given Minecraft version and loader /// If `loader_version` is `None`, it will return the latest stable version else /// it will return the specified version if found +#[tracing::instrument] pub async fn get_loader_version( mc_version: &str, loader: GameLoader, @@ -405,6 +411,7 @@ pub async fn get_loader_version( .loaders .iter() .find(|it| loader_version.map_or(it.stable, |version| it.id == version)) + .or_else(|| loaders.loaders.first()) .cloned() .ok_or(MetadataError::NoMatchingLoader)?; diff --git a/packages/core/src/api/java/dao.rs b/packages/core/src/api/java/dao.rs index 7d7db59c..394fd20c 100644 --- a/packages/core/src/api/java/dao.rs +++ b/packages/core/src/api/java/dao.rs @@ -7,6 +7,8 @@ use crate::{error::LauncherResult, store::State}; use super::{JavaError, JavaInfo}; +pub type JavaVersionId = i64; + /// Returns the latest Java version pub async fn get_latest_java() -> LauncherResult> { let db = &State::get().await?.db; @@ -50,7 +52,7 @@ pub async fn get_java_all() -> LauncherResult> { } /// Returns the specific Java version by ID -pub async fn get_java_by_id(id: u64) -> LauncherResult> { +pub async fn get_java_by_id(id: JavaVersionId) -> LauncherResult> { let db = &State::get().await?.db; Ok(java_versions::Entity::find() diff --git a/packages/core/src/api/processes.rs b/packages/core/src/api/processes.rs index ef3ecb8f..70f38437 100644 --- a/packages/core/src/api/processes.rs +++ b/packages/core/src/api/processes.rs @@ -4,6 +4,9 @@ use crate::store::processes::Process; #[derive(Debug, Serialize)] pub enum ProcessPayload { + Starting { + command: String, + }, Started { process: Process, }, diff --git a/packages/core/src/api/proxy/impl/proxy_cli.rs b/packages/core/src/api/proxy/impl/proxy_cli.rs index 7b1307af..0a72b9ca 100644 --- a/packages/core/src/api/proxy/impl/proxy_cli.rs +++ b/packages/core/src/api/proxy/impl/proxy_cli.rs @@ -4,7 +4,7 @@ use indicatif::ProgressBar; use tokio::sync::RwLock; use uuid::Uuid; -use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult}; +use crate::{api::{processes::ProcessPayload, proxy::{event::LauncherEvent, LauncherProxy}}, constants::CLI_TOTAL_INGRESS, LauncherResult}; #[derive(Default, Debug)] pub struct ProxyCli { @@ -49,9 +49,22 @@ impl LauncherProxy for ProxyCli { LauncherEvent::Message(message) => { println!("[{:?}] {}", message.level, message.message); }, - _ => { - println!("{:#?}", event); - }, + LauncherEvent::Process(process) => { + match process { + ProcessPayload::Starting { command } => { + println!("Starting process: {command}"); + }, + ProcessPayload::Started { process } => { + println!("Process started: {process:#?}"); + }, + ProcessPayload::Stopped { pid, exit_code } => { + println!("Process {pid} exited with code {exit_code}"); + }, + ProcessPayload::Output { pid, output } => { + println!("Process {pid}: {output}"); + }, + } + } } Ok(()) diff --git a/packages/core/src/api/setting_profiles.rs b/packages/core/src/api/setting_profiles.rs index 038d656c..71dd6239 100644 --- a/packages/core/src/api/setting_profiles.rs +++ b/packages/core/src/api/setting_profiles.rs @@ -1,6 +1,7 @@ use onelauncher_entity::setting_profiles; +use sea_orm::ActiveValue::Set; -use crate::store::State; +use crate::{error::LauncherResult, store::State}; pub mod dao { use onelauncher_entity::setting_profiles; @@ -65,6 +66,7 @@ pub mod dao { } } +/// Returns the global settings profile pub async fn get_global_profile() -> setting_profiles::Model { let Ok(state) = State::get().await else { return setting_profiles::Model::default_global_profile(); @@ -73,3 +75,16 @@ pub async fn get_global_profile() -> setting_profiles::Model { let settings = state.settings.read().await; settings.global_game_settings.clone() } + +/// Creates a new setting profile and inserts it into the database. Returns the inserted entry +pub async fn create_profile(name: &str, block: F) -> LauncherResult +where F: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult { + let model = setting_profiles::ActiveModel { + name: Set(name.to_string()), + ..Default::default() + }; + + let model = block(model).await?; + + dao::insert_profile(model).await +} diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 41a932c2..b827a9a9 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -33,6 +33,8 @@ pub enum LauncherError { ReqwestError(#[from] reqwest::Error), #[error(transparent)] InterpulseError(#[from] interpulse::Error), + #[error(transparent)] + RegexError(#[from] regex::Error), #[error("couldn't acquire semaphore: {0}")] SemaphoreError(#[from] tokio::sync::AcquireError), diff --git a/packages/core/src/store/metadata.rs b/packages/core/src/store/metadata.rs index 613d2cd0..c4df95f5 100644 --- a/packages/core/src/store/metadata.rs +++ b/packages/core/src/store/metadata.rs @@ -59,6 +59,7 @@ impl Metadata { } + #[tracing::instrument(skip(self))] pub async fn get_vanilla_or_fetch(&mut self) -> LauncherResult<&VanillaManifest> { if !self.initialized() { self.initialize().await?; @@ -67,6 +68,7 @@ impl Metadata { self.get_vanilla() } + #[tracing::instrument(skip(self))] pub async fn get_modded_or_fetch(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> { if !loader.is_modded() { return Err(MetadataError::NotModdedManifest(loader).into()); diff --git a/packages/core/src/store/processes.rs b/packages/core/src/store/processes.rs index 4c493fef..baf7974c 100644 --- a/packages/core/src/store/processes.rs +++ b/packages/core/src/store/processes.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::PathBuf; use std::process::Stdio; use std::sync::Arc; @@ -12,10 +13,11 @@ use tokio::task::JoinHandle; use uuid::Uuid; use crate::api::cluster; -use crate::api::game::log::censor_line; +use crate::api::cluster::dao::ClusterId; +use crate::api::game::log::{censor_line, CensorMap}; use crate::api::processes::ProcessPayload; use crate::api::proxy::event::{send_event, LauncherEvent}; -use crate::error::{DaoError, LauncherResult}; +use crate::error::LauncherResult; use crate::send_error; use crate::utils::io::IOError; @@ -39,7 +41,7 @@ pub struct ProcessStore { pub struct Process { pub pid: u32, pub started_at: DateTime, - pub cluster_id: u64, + pub cluster_id: ClusterId, pub post_hook: Option, pub account_id: Uuid, #[serde(skip)] @@ -79,7 +81,7 @@ impl ChildType { Self::Owned(child) => child .try_wait() .map_err(IOError::from)? - .map(|s| s.code().unwrap_or_default()), + .and_then(|s| s.code()), }) } @@ -109,13 +111,83 @@ impl ProcessStore { Self::default() } + /// Returns a list of all running processes for a given cluster. + pub async fn get_running_by_cluster( + &self, + cluster_id: ClusterId, + ) -> Vec>> { + let mut list = Vec::new(); + let read_guard = self.processes.read().await; + + for process in read_guard.values() { + if process.read().await.cluster_id == cluster_id { + list.push(process.clone()); + } + } + + list + } + + /// Returns a list of all running processes for a given account. + pub async fn get_running_by_account( + &self, + account_id: Uuid, + ) -> Vec>> { + let mut list = Vec::new(); + let read_guard = self.processes.read().await; + + for process in read_guard.values() { + if process.read().await.account_id == account_id { + list.push(process.clone()); + } + } + + list + } + + /// Returns a list of all running processes. + pub async fn get_running(&self) -> Vec>> { + let mut list = Vec::new(); + let read_guard = self.processes.read().await; + + for process in read_guard.values() { + list.push(process.clone()); + } + + list + } + + /// Returns whether a cluster has a running process. + pub async fn has_running(&self, cluster_id: ClusterId) -> bool { + let read_guard = self.processes.read().await; + + for process in read_guard.values() { + if process.read().await.cluster_id == cluster_id { + return true; + } + } + + false + } + + /// Spawns a new process (Minecraft Instance) pub async fn spawn( &self, - cluster_id: u64, + cluster_id: ClusterId, + cwd: PathBuf, creds: MinecraftCredentials, + censors: CensorMap, settings: &setting_profiles::Model, mut command: Command, ) -> LauncherResult>> { + send_event(LauncherEvent::Process(ProcessPayload::Starting { command: format!("{command:?}") })).await; + + if let Some(pre_hook) = &settings.hook_pre { + if let Err(err) = run_hook(pre_hook, cwd.clone()).await { + send_error!("{err:?}"); + } + } + command .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -140,20 +212,19 @@ impl ProcessStore { } } - let account_id = creds.id; tokio::spawn(async move { let mut stdout = BufReader::new(stdout).lines(); let mut stderr = BufReader::new(stderr).lines(); - while let Ok(Some(line)) = tokio::select! { + while let Ok(Some(mut line)) = tokio::select! { line = stdout.next_line() => line, line = stderr.next_line() => line, } { - let censored = censor_line(&creds, line); + censor_line(&censors, &mut line); send_event(LauncherEvent::Process(ProcessPayload::Output { pid, - output: censored, + output: line, })).await; } }); @@ -164,10 +235,10 @@ impl ProcessStore { started_at, cluster_id, post_hook: settings.hook_post.clone(), - account_id, + account_id: creds.id, child_type: child_type.clone(), process_loop: Some(tokio::task::spawn(Self::process_loop( - cluster_id, settings.hook_post.clone(), child_type, + cluster_id, cwd, settings.hook_post.clone(), child_type, ))), }; @@ -185,7 +256,8 @@ impl ProcessStore { } async fn process_loop( - cluster_id: u64, + cluster_id: ClusterId, + cwd: PathBuf, post_hook: Option, child: Arc>, ) -> LauncherResult { @@ -221,22 +293,8 @@ impl ProcessStore { tracing::debug!("removed process with pid {}", pid); if let Some(post_hook) = post_hook { - if !post_hook.is_empty() { - let result: LauncherResult<()> = { - let cluster = cluster::dao::get_cluster_by_id(cluster_id) - .await? - .ok_or(DaoError::NotFound)?; - - if let Err(err) = run_hook(&post_hook, &cluster.path).await { - send_error!("{err:?}"); - } - - Ok(()) - }; - - if let Err(err) = result { - send_error!("failed to run post hook: {}", err); - } + if let Err(err) = run_hook(&post_hook, cwd).await { + send_error!("{err:?}"); } } @@ -246,13 +304,14 @@ impl ProcessStore { } } -pub async fn run_hook(hook: &str, cwd: &str) -> LauncherResult> { +async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult> { let mut split = hook.split(' '); let Some(command) = split.next() else { return Ok(None); }; + tracing::debug!("running hook"); let result = Command::new(command) .args(split) .current_dir(cwd) diff --git a/packages/core/src/utils/io.rs b/packages/core/src/utils/io.rs index 6f9b78e9..e6e80e00 100644 --- a/packages/core/src/utils/io.rs +++ b/packages/core/src/utils/io.rs @@ -8,6 +8,8 @@ use serde::de::DeserializeOwned; use tempfile::TempDir; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_util::compat::TokioAsyncWriteCompatExt; + /// A wrapper around generic and unhelpful [`std::io::Error`] messages. #[derive(Debug, thiserror::Error)] pub enum IOError { @@ -215,6 +217,17 @@ pub async fn copy( }) } +// /// Copies an async reader into an async writer. Returns the bytes copied +// pub async fn copy_buf<'a, R, W>(reader: &'a mut R, writer: &'a mut W) -> Result +// where +// R: TokioAsyncReadCompatExt, +// W: TokioAsyncWriteCompatExt, +// { +// tokio::io::copy_buf(reader, writer) +// .await +// .map_err(IOError::from) +// } + /// Removes a file from the filesystem. pub async fn remove_file(path: impl AsRef) -> Result<(), IOError> { let path = path.as_ref(); @@ -237,10 +250,7 @@ pub fn tempfile() -> Result { } /// Reads a zip archive from a byte array -pub async fn read_zip_entries_bytes( - data: Vec, - mut f: F, -) -> Result<(), IOError> +pub async fn read_zip_entries_bytes(data: Vec, mut f: F) -> Result<(), IOError> where F: AsyncFnMut( usize, @@ -272,11 +282,26 @@ where pub async fn unzip_bytes( data: Vec, dest_path: impl AsRef, +) -> Result<(), IOError> { + unzip_bytes_filtered(data, None:: bool>, dest_path).await +} + +/// Unzips a zip archive from a byte array +pub async fn unzip_bytes_filtered( + data: Vec, + filter_entries: Option bool>, + dest_path: impl AsRef, ) -> Result<(), IOError> { read_zip_entries_bytes(data, async |_, entry, entry_reader| { - let path = dest_path - .as_ref() - .join(sanitize_path(entry.filename().as_str()?)); + let file_name = entry.filename().as_str()?; + + if let Some(filter) = &filter_entries { + if !filter(file_name) { + return Ok(()); + } + } + + let path = dest_path.as_ref().join(sanitize_path(file_name)); let is_dir = entry.dir()?; @@ -288,20 +313,14 @@ pub async fn unzip_bytes( } let file = tokio::fs::File::create(&path).await?; - let mut writer = tokio::io::BufWriter::new(file); + let writer = tokio::io::BufWriter::new(file); - let mut buf = vec![0; 8192]; - loop { - let n = futures::AsyncReadExt::read(entry_reader, &mut buf).await?; - if n == 0 { - break; - } - writer.write_all(&buf[..n]).await?; - } + futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?; } Ok(()) - }).await?; + }) + .await?; Ok(()) } @@ -331,17 +350,10 @@ pub async fn unzip_file( } let file = tokio::fs::File::create(&path).await?; - let mut writer = tokio::io::BufWriter::new(file); - let mut entry_reader = reader.reader_without_entry(index).await?; + let writer = tokio::io::BufWriter::new(file); + let entry_reader = reader.reader_without_entry(index).await?; - let mut buf = vec![0; 8192]; - loop { - let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?; - if n == 0 { - break; - } - writer.write_all(&buf[..n]).await?; - } + futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?; } } diff --git a/packages/entity/src/domain/mod.rs b/packages/entity/src/domain/mod.rs index c371a48e..2f991ef8 100644 --- a/packages/entity/src/domain/mod.rs +++ b/packages/entity/src/domain/mod.rs @@ -1,4 +1,5 @@ pub mod cluster_stage; pub mod icon; pub mod loader; +pub mod resolution; pub mod settings; \ No newline at end of file diff --git a/packages/entity/src/domain/resolution.rs b/packages/entity/src/domain/resolution.rs new file mode 100644 index 00000000..a7ec1410 --- /dev/null +++ b/packages/entity/src/domain/resolution.rs @@ -0,0 +1,58 @@ +use sea_orm::{sea_query::{Nullable, ValueType}, TryGetableFromJson}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Resolution { + pub width: u32, + pub height: u32, +} + +impl Default for Resolution { + fn default() -> Self { + Self::new(854, 480) + } +} + +impl Resolution { + pub fn new(width: u32, height: u32) -> Self { + Self { width, height } + } +} + +impl From for sea_orm::Value { + fn from(value: Resolution) -> Self { + sea_orm::Value::Json(serde_json::to_value(value).ok().map(Box::new)) + } +} + +impl TryGetableFromJson for Resolution {} + +impl ValueType for Resolution { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Json(Some(json)) => Ok( + serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?, + ), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "Resolution".to_string() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::Json + } + + fn column_type() -> sea_orm::ColumnType { + sea_orm::ColumnType::Json + } +} + +impl Nullable for Resolution { + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } +} + diff --git a/packages/entity/src/entities/cluster_groups.rs b/packages/entity/src/entities/cluster_groups.rs index 5d1b8238..fec66c73 100644 --- a/packages/entity/src/entities/cluster_groups.rs +++ b/packages/entity/src/entities/cluster_groups.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key)] - pub id: u64, + pub id: i64, #[sea_orm(column_type = "Text")] pub name: String, } diff --git a/packages/entity/src/entities/cluster_packages.rs b/packages/entity/src/entities/cluster_packages.rs index 11435101..1be43f7a 100644 --- a/packages/entity/src/entities/cluster_packages.rs +++ b/packages/entity/src/entities/cluster_packages.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub cluster_id: u64, + pub cluster_id: i64, #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] pub package_hash: String, } diff --git a/packages/entity/src/entities/clusters.rs b/packages/entity/src/entities/clusters.rs index ddf84a08..cd4ff0a3 100644 --- a/packages/entity/src/entities/clusters.rs +++ b/packages/entity/src/entities/clusters.rs @@ -8,12 +8,12 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key)] - pub id: u64, + pub id: i64, #[sea_orm(column_type = "Text")] pub path: String, pub stage: ClusterStage, pub created_at: i64, - pub group_id: Option, + pub group_id: Option, #[sea_orm(column_type = "Text")] pub name: String, #[sea_orm(column_type = "Text")] diff --git a/packages/entity/src/entities/java_versions.rs b/packages/entity/src/entities/java_versions.rs index 2a9fcffb..e52ceb72 100644 --- a/packages/entity/src/entities/java_versions.rs +++ b/packages/entity/src/entities/java_versions.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key)] - pub id: u64, + pub id: i64, pub major_version: u32, #[sea_orm(column_type = "Text")] pub vendor_name: String, diff --git a/packages/entity/src/entities/setting_profiles.rs b/packages/entity/src/entities/setting_profiles.rs index 9a38db8a..d8071af9 100644 --- a/packages/entity/src/entities/setting_profiles.rs +++ b/packages/entity/src/entities/setting_profiles.rs @@ -4,7 +4,7 @@ use merge::Merge; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::settings::SettingsOsExtra; +use crate::{resolution::Resolution, settings::SettingsOsExtra}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, Merge)] #[sea_orm(table_name = "setting_profiles")] @@ -14,9 +14,9 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] #[merge(skip)] pub name: String, - pub java_id: Option, - pub res_w: Option, - pub res_h: Option, + pub java_id: Option, + #[sea_orm(column_type = "Text", nullable)] + pub res: Option, pub force_fullscreen: Option, pub mem_max: Option, #[sea_orm(column_type = "Text", nullable)] @@ -84,9 +84,8 @@ impl Model { java_id: None, launch_args: None, launch_env: None, - mem_max: Some(3072), - res_h: None, - res_w: None, + mem_max: Some(2048), + res: None, os_extra: Some(SettingsOsExtra::default()) } } diff --git a/packages/migration/src/init.sql b/packages/migration/src/init.sql index 98753847..bda6bb1c 100644 --- a/packages/migration/src/init.sql +++ b/packages/migration/src/init.sql @@ -33,8 +33,7 @@ create table packages ( create table setting_profiles ( name text not null, java_id integer, - res_w integer, - res_h integer, + res json_text, force_fullscreen boolean, mem_max integer, launch_args text,