diff --git a/.changeset/custom_game_arguments.md b/.changeset/custom_game_arguments.md new file mode 100644 index 00000000..19df21a7 --- /dev/null +++ b/.changeset/custom_game_arguments.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# Game settings have a Game Arguments field for passing extra arguments to Minecraft, globally or per cluster diff --git a/.sqlx/query-f68b9ade92feb989828ca81582c5960bf2968c4b75705e1a0e6800e97c3f4628.json b/.sqlx/query-01b2ef24541af92505a3c2bc14037a61470a393dffa06da8bebfba98138709d0.json similarity index 86% rename from .sqlx/query-f68b9ade92feb989828ca81582c5960bf2968c4b75705e1a0e6800e97c3f4628.json rename to .sqlx/query-01b2ef24541af92505a3c2bc14037a61470a393dffa06da8bebfba98138709d0.json index c1c2145d..c93c3499 100644 --- a/.sqlx/query-f68b9ade92feb989828ca81582c5960bf2968c4b75705e1a0e6800e97c3f4628.json +++ b/.sqlx/query-01b2ef24541af92505a3c2bc14037a61470a393dffa06da8bebfba98138709d0.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\tFROM setting_profiles\n\t\tORDER BY name ASC\n\t\t", + "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\tFROM setting_profiles\n\t\tORDER BY name ASC\n\t\t", "describe": { "columns": [ { @@ -70,9 +70,20 @@ } }, { - "name": "launch_env", + "name": "game_args", "ordinal": 6, "type_info": "Text", + "origin": { + "Table": { + "table": "setting_profiles", + "name": "game_args" + } + } + }, + { + "name": "launch_env", + "ordinal": 7, + "type_info": "Text", "origin": { "Table": { "table": "setting_profiles", @@ -82,7 +93,7 @@ }, { "name": "hook_pre", - "ordinal": 7, + "ordinal": 8, "type_info": "Text", "origin": { "Table": { @@ -93,7 +104,7 @@ }, { "name": "hook_wrapper", - "ordinal": 8, + "ordinal": 9, "type_info": "Text", "origin": { "Table": { @@ -104,7 +115,7 @@ }, { "name": "hook_post", - "ordinal": 9, + "ordinal": 10, "type_info": "Text", "origin": { "Table": { @@ -115,7 +126,7 @@ }, { "name": "os_extra", - "ordinal": 10, + "ordinal": 11, "type_info": "Text", "origin": { "Table": { @@ -126,7 +137,7 @@ }, { "name": "browser_update_mode", - "ordinal": 11, + "ordinal": 12, "type_info": "Text", "origin": { "Table": { @@ -151,8 +162,9 @@ true, true, true, + true, true ] }, - "hash": "f68b9ade92feb989828ca81582c5960bf2968c4b75705e1a0e6800e97c3f4628" + "hash": "01b2ef24541af92505a3c2bc14037a61470a393dffa06da8bebfba98138709d0" } diff --git a/.sqlx/query-c939cef4b51d506d5b73b4c2e0330ba45423c0116c911b9da16576290ed9f3c2.json b/.sqlx/query-598a34a0eb0f4d497cfd9bd77696310d1088e8baec677e07843a21ce6978652e.json similarity index 72% rename from .sqlx/query-c939cef4b51d506d5b73b4c2e0330ba45423c0116c911b9da16576290ed9f3c2.json rename to .sqlx/query-598a34a0eb0f4d497cfd9bd77696310d1088e8baec677e07843a21ce6978652e.json index 72745260..8d303903 100644 --- a/.sqlx/query-c939cef4b51d506d5b73b4c2e0330ba45423c0116c911b9da16576290ed9f3c2.json +++ b/.sqlx/query-598a34a0eb0f4d497cfd9bd77696310d1088e8baec677e07843a21ce6978652e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tINSERT INTO setting_profiles (\n\t\t\tname, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t\thook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(name) DO UPDATE SET\n\t\t\tjava_path = excluded.java_path,\n\t\t\tresolution = excluded.resolution,\n\t\t\tforce_fullscreen = excluded.force_fullscreen,\n\t\t\tmem_max = excluded.mem_max,\n\t\t\tlaunch_args = excluded.launch_args,\n\t\t\tlaunch_env = excluded.launch_env,\n\t\t\thook_pre = excluded.hook_pre,\n\t\t\thook_wrapper = excluded.hook_wrapper,\n\t\t\thook_post = excluded.hook_post,\n\t\t\tos_extra = excluded.os_extra,\n\t\t\tbrowser_update_mode = excluded.browser_update_mode\n\t\tRETURNING name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\t", + "query": "\n\t\tINSERT INTO setting_profiles (\n\t\t\tname, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n\t\t\thook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(name) DO UPDATE SET\n\t\t\tjava_path = excluded.java_path,\n\t\t\tresolution = excluded.resolution,\n\t\t\tforce_fullscreen = excluded.force_fullscreen,\n\t\t\tmem_max = excluded.mem_max,\n\t\t\tlaunch_args = excluded.launch_args,\n\t\t\tgame_args = excluded.game_args,\n\t\t\tlaunch_env = excluded.launch_env,\n\t\t\thook_pre = excluded.hook_pre,\n\t\t\thook_wrapper = excluded.hook_wrapper,\n\t\t\thook_post = excluded.hook_post,\n\t\t\tos_extra = excluded.os_extra,\n\t\t\tbrowser_update_mode = excluded.browser_update_mode\n\t\tRETURNING name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\t", "describe": { "columns": [ { @@ -70,9 +70,20 @@ } }, { - "name": "launch_env", + "name": "game_args", "ordinal": 6, "type_info": "Text", + "origin": { + "Table": { + "table": "setting_profiles", + "name": "game_args" + } + } + }, + { + "name": "launch_env", + "ordinal": 7, + "type_info": "Text", "origin": { "Table": { "table": "setting_profiles", @@ -82,7 +93,7 @@ }, { "name": "hook_pre", - "ordinal": 7, + "ordinal": 8, "type_info": "Text", "origin": { "Table": { @@ -93,7 +104,7 @@ }, { "name": "hook_wrapper", - "ordinal": 8, + "ordinal": 9, "type_info": "Text", "origin": { "Table": { @@ -104,7 +115,7 @@ }, { "name": "hook_post", - "ordinal": 9, + "ordinal": 10, "type_info": "Text", "origin": { "Table": { @@ -115,7 +126,7 @@ }, { "name": "os_extra", - "ordinal": 10, + "ordinal": 11, "type_info": "Text", "origin": { "Table": { @@ -126,7 +137,7 @@ }, { "name": "browser_update_mode", - "ordinal": 11, + "ordinal": 12, "type_info": "Text", "origin": { "Table": { @@ -137,7 +148,7 @@ } ], "parameters": { - "Right": 12 + "Right": 13 }, "nullable": [ false, @@ -151,8 +162,9 @@ true, true, true, + true, true ] }, - "hash": "c939cef4b51d506d5b73b4c2e0330ba45423c0116c911b9da16576290ed9f3c2" + "hash": "598a34a0eb0f4d497cfd9bd77696310d1088e8baec677e07843a21ce6978652e" } diff --git a/.sqlx/query-98500a40f7e1ca7df79dd0ca90eaaa4f0b079ccbd757e5db1a8229ea58d2e700.json b/.sqlx/query-86e3ba70da419464c0be3b3971f613c5a112558472cf2dcdeffa5907e3785318.json similarity index 86% rename from .sqlx/query-98500a40f7e1ca7df79dd0ca90eaaa4f0b079ccbd757e5db1a8229ea58d2e700.json rename to .sqlx/query-86e3ba70da419464c0be3b3971f613c5a112558472cf2dcdeffa5907e3785318.json index e08f44bc..bd9fbeb1 100644 --- a/.sqlx/query-98500a40f7e1ca7df79dd0ca90eaaa4f0b079ccbd757e5db1a8229ea58d2e700.json +++ b/.sqlx/query-86e3ba70da419464c0be3b3971f613c5a112558472cf2dcdeffa5907e3785318.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\tFROM setting_profiles\n\t\tWHERE name = ?\n\t\t", + "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\tFROM setting_profiles\n\t\tWHERE name = ?\n\t\t", "describe": { "columns": [ { @@ -70,9 +70,20 @@ } }, { - "name": "launch_env", + "name": "game_args", "ordinal": 6, "type_info": "Text", + "origin": { + "Table": { + "table": "setting_profiles", + "name": "game_args" + } + } + }, + { + "name": "launch_env", + "ordinal": 7, + "type_info": "Text", "origin": { "Table": { "table": "setting_profiles", @@ -82,7 +93,7 @@ }, { "name": "hook_pre", - "ordinal": 7, + "ordinal": 8, "type_info": "Text", "origin": { "Table": { @@ -93,7 +104,7 @@ }, { "name": "hook_wrapper", - "ordinal": 8, + "ordinal": 9, "type_info": "Text", "origin": { "Table": { @@ -104,7 +115,7 @@ }, { "name": "hook_post", - "ordinal": 9, + "ordinal": 10, "type_info": "Text", "origin": { "Table": { @@ -115,7 +126,7 @@ }, { "name": "os_extra", - "ordinal": 10, + "ordinal": 11, "type_info": "Text", "origin": { "Table": { @@ -126,7 +137,7 @@ }, { "name": "browser_update_mode", - "ordinal": 11, + "ordinal": 12, "type_info": "Text", "origin": { "Table": { @@ -151,8 +162,9 @@ true, true, true, + true, true ] }, - "hash": "98500a40f7e1ca7df79dd0ca90eaaa4f0b079ccbd757e5db1a8229ea58d2e700" + "hash": "86e3ba70da419464c0be3b3971f613c5a112558472cf2dcdeffa5907e3785318" } diff --git a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs index 64a3065b..6302c257 100644 --- a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs +++ b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs @@ -175,6 +175,7 @@ impl Component for ClusterSettings { .into_element(), ) .child(text_row(cluster_id, TextField::JvmArgs, &profile, &global)) + .child(text_row(cluster_id, TextField::GameArgs, &profile, &global)) .child(section_header("PROCESS")) .child(text_row(cluster_id, TextField::Pre, &profile, &global)) .child(text_row(cluster_id, TextField::Wrapper, &profile, &global)) @@ -629,6 +630,7 @@ impl Component for ResolutionRow { #[derive(Clone, Copy, PartialEq)] enum TextField { JvmArgs, + GameArgs, Pre, Wrapper, Post, @@ -643,6 +645,12 @@ impl TextField { "Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.", "-XX:+UseG1GC", ), + Self::GameArgs => ( + IconType::Play, + "Game Arguments", + "Extra arguments passed to Minecraft. Separate them with spaces; quote values containing spaces.", + "--tracy --tracyNoImages", + ), Self::Pre => ( IconType::FilePlus02, "Pre-Launch Command", @@ -667,6 +675,7 @@ impl TextField { fn value(self, profile: &GameSettingsProfile) -> Option { match self { Self::JvmArgs => profile.launch_args.clone(), + Self::GameArgs => profile.game_args.clone(), Self::Pre => profile.hook_pre.clone(), Self::Wrapper => profile.hook_wrapper.clone(), Self::Post => profile.hook_post.clone(), @@ -677,6 +686,7 @@ impl TextField { let mut u = ProfileUpdate::default(); match self { Self::JvmArgs => u.launch_args = value, + Self::GameArgs => u.game_args = value, Self::Pre => u.hook_pre = value, Self::Wrapper => u.hook_wrapper = value, Self::Post => u.hook_post = value, diff --git a/packages/oneclient_app/src/view/app/settings/minecraft.rs b/packages/oneclient_app/src/view/app/settings/minecraft.rs index 0fb71eb1..0466f203 100644 --- a/packages/oneclient_app/src/view/app/settings/minecraft.rs +++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs @@ -48,6 +48,10 @@ impl Component for SettingsMinecraft { let v = profile.launch_args.clone().unwrap_or_default(); move || v }); + let game_args = use_state({ + let v = profile.game_args.clone().unwrap_or_default(); + move || v + }); let pre_launch_command = use_state({ let v = profile.hook_pre.clone().unwrap_or_default(); move || v @@ -79,6 +83,7 @@ impl Component for SettingsMinecraft { &height.read(), &memory.read(), &jvm_args.read(), + &game_args.read(), &pre_launch_command.read(), &wrapper_command.read(), &post_exit_command.read(), @@ -154,6 +159,19 @@ impl Component for SettingsMinecraft { defaults.launch_args.clone().unwrap_or_default(), ), )) + .child(settings_row( + IconType::Play, + "Game Arguments", + "Extra arguments passed to Minecraft. Separate them with spaces; quote values containing spaces.", + resettable( + TextInput::new(game_args) + .placeholder("--tracy --tracyNoImages") + .expandable(true) + .width(Size::px(220.)), + game_args, + defaults.game_args.clone().unwrap_or_default(), + ), + )) .child(section_header("CONTENT")) .child(settings_row( IconType::RefreshCw01, @@ -263,6 +281,7 @@ fn build_update( height: &str, memory: &str, jvm_args: &str, + game_args: &str, pre: &str, wrapper: &str, post: &str, @@ -286,6 +305,7 @@ fn build_update( resolution, mem_max, launch_args: command_patch(jvm_args), + game_args: command_patch(game_args), hook_pre: command_patch(pre), hook_wrapper: command_patch(wrapper), hook_post: command_patch(post), diff --git a/packages/oneclient_cluster/src/profile.rs b/packages/oneclient_cluster/src/profile.rs index c05b030d..a855bc05 100644 --- a/packages/oneclient_cluster/src/profile.rs +++ b/packages/oneclient_cluster/src/profile.rs @@ -12,6 +12,7 @@ pub struct GameSettingsProfile { pub force_fullscreen: Option, pub mem_max: Option, pub launch_args: Option, + pub game_args: Option, pub launch_env: Option, pub hook_pre: Option, pub hook_wrapper: Option, @@ -57,6 +58,7 @@ impl GameSettingsProfile { force_fullscreen: Some(false), mem_max: Some(oneclient_common::default_mem_max()), launch_args: None, + game_args: None, launch_env: None, hook_pre: None, hook_wrapper: None, @@ -90,6 +92,9 @@ impl GameSettingsProfile { if self.launch_args.is_none() { self.launch_args = global.launch_args.clone(); } + if self.game_args.is_none() { + self.game_args = global.game_args.clone(); + } if self.launch_env.is_none() { self.launch_env = global.launch_env.clone(); } @@ -121,6 +126,7 @@ impl GameSettingsProfile { force_fullscreen: row.force_fullscreen.map(|v| v != 0), mem_max: row.mem_max.map(|v| v as u32), launch_args: row.launch_args, + game_args: row.game_args, launch_env: row.launch_env, hook_pre: row.hook_pre, hook_wrapper: row.hook_wrapper, @@ -149,6 +155,7 @@ impl GameSettingsProfile { force_fullscreen: self.force_fullscreen.map(i64::from), mem_max: self.mem_max.map(i64::from), launch_args: self.launch_args.clone(), + game_args: self.game_args.clone(), launch_env: self.launch_env.clone(), hook_pre: self.hook_pre.clone(), hook_wrapper: self.hook_wrapper.clone(), diff --git a/packages/oneclient_cluster/src/profiles.rs b/packages/oneclient_cluster/src/profiles.rs index 45b010b4..6bbff31f 100644 --- a/packages/oneclient_cluster/src/profiles.rs +++ b/packages/oneclient_cluster/src/profiles.rs @@ -115,6 +115,7 @@ pub struct ProfileUpdate { pub force_fullscreen: Patch, pub mem_max: Patch, pub launch_args: Patch, + pub game_args: Patch, pub launch_env: Patch, pub hook_pre: Patch, pub hook_wrapper: Patch, @@ -132,6 +133,8 @@ impl ProfileUpdate { self.mem_max.apply_to_option(&mut profile.mem_max); self.launch_args .apply_to_command_option(&mut profile.launch_args); + self.game_args + .apply_to_command_option(&mut profile.game_args); self.launch_env .apply_to_command_option(&mut profile.launch_env); self.hook_pre.apply_to_command_option(&mut profile.hook_pre); diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index ed0ea6fb..3eca87da 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -360,7 +360,11 @@ async fn start( profile.resolution.unwrap_or_default(), &java.os_arch, )?; - arguments::append_profile_game_arguments(&mut mc_args, profile.force_fullscreen, None); + arguments::append_profile_game_arguments( + &mut mc_args, + profile.force_fullscreen, + profile.game_args.as_deref(), + ); if let Some(reason) = run_hook(profile.hook_pre.as_deref(), &cwd).await { events diff --git a/packages/oneclient_db/migrations/20261002120000_setting_profile_game_args.sql b/packages/oneclient_db/migrations/20261002120000_setting_profile_game_args.sql new file mode 100644 index 00000000..b2e5b660 --- /dev/null +++ b/packages/oneclient_db/migrations/20261002120000_setting_profile_game_args.sql @@ -0,0 +1,3 @@ +-- Extra arguments appended to the Minecraft command line, after the version's +-- own game arguments. NULL inherits the global profile like `launch_args`. +ALTER TABLE `setting_profiles` ADD COLUMN `game_args` TEXT; diff --git a/packages/oneclient_db/src/dao/setting_profile.rs b/packages/oneclient_db/src/dao/setting_profile.rs index 0511ece1..8ecf3636 100644 --- a/packages/oneclient_db/src/dao/setting_profile.rs +++ b/packages/oneclient_db/src/dao/setting_profile.rs @@ -14,7 +14,7 @@ pub async fn list_all(pool: &SqlitePool) -> DbResult> { let rows = sqlx::query_as!( SettingProfileRow, r#" - SELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env, + SELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env, hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode FROM setting_profiles ORDER BY name ASC @@ -30,7 +30,7 @@ pub async fn get_by_name(pool: &SqlitePool, name: &str) -> DbResult, pub mem_max: Option, pub launch_args: Option, + pub game_args: Option, pub launch_env: Option, pub hook_pre: Option, pub hook_wrapper: Option, diff --git a/packages/oneclient_mc/src/arguments.rs b/packages/oneclient_mc/src/arguments.rs index adf91ff8..df327ef2 100644 --- a/packages/oneclient_mc/src/arguments.rs +++ b/packages/oneclient_mc/src/arguments.rs @@ -232,31 +232,27 @@ pub fn minecraft_arguments( } fn drop_repeated_arguments(args: &mut Vec) { - let mut seen: HashSet<(String, Option)> = HashSet::new(); - let mut kept: Vec = Vec::with_capacity(args.len()); - let mut index = 0; + let mut seen = HashSet::new(); + retain_arguments(args, |option, value| { + seen.insert((option.to_owned(), value.map(str::to_owned))) + }); +} - while index < args.len() { - let token = &args[index]; +fn retain_arguments(args: &mut Vec, mut keep: impl FnMut(&str, Option<&str>) -> bool) { + let mut kept = Vec::with_capacity(args.len()); + let mut tokens = std::mem::take(args).into_iter().peekable(); + + while let Some(token) = tokens.next() { if !token.starts_with("--") { - kept.push(token.clone()); - index += 1; + kept.push(token); continue; } - let value = args - .get(index + 1) - .filter(|next| !next.starts_with("--")) - .cloned(); - - if seen.insert((token.clone(), value.clone())) { - kept.push(token.clone()); - if let Some(value) = &value { - kept.push(value.clone()); - } + let value = tokens.next_if(|next| !next.starts_with("--")); + if keep(&token, value.as_deref()) { + kept.push(token); + kept.extend(value); } - - index += if value.is_some() { 2 } else { 1 }; } *args = kept; @@ -265,15 +261,17 @@ fn drop_repeated_arguments(args: &mut Vec) { pub fn append_profile_game_arguments( args: &mut Vec, force_fullscreen: Option, - launch_args: Option<&str>, + game_args: Option<&str>, ) { if force_fullscreen.unwrap_or(false) { args.push("--fullscreen".to_string()); } - if let Some(extra) = launch_args.map(str::trim).filter(|s| !s.is_empty()) { - args.push(extra.to_string()); - } + let extra = game_args.map(split_custom_args).unwrap_or_default(); + retain_arguments(args, |option, _| { + option == "--tweakClass" || !extra.iter().any(|arg| arg == option) + }); + args.extend(extra); } pub fn processor_arguments, S: std::hash::BuildHasher>( @@ -599,8 +597,8 @@ where #[cfg(test)] mod tests { use super::{ - HashMap, Library, Path, SidedDataEntry, ZGC_MIN_HEAP_MB, classpaths, - drop_repeated_arguments, get_library, is_collector_flag, java_arguments, + HashMap, Library, Path, SidedDataEntry, ZGC_MIN_HEAP_MB, append_profile_game_arguments, + classpaths, drop_repeated_arguments, get_library, is_collector_flag, java_arguments, minecraft_arguments, performance_flags, processor_arguments, split_custom_args, }; use oneclient_common::Resolution; @@ -627,6 +625,32 @@ mod tests { ); } + #[test] + fn game_arguments_are_split_after_fullscreen() { + let mut args = ["--username", "Steve", "--width", "854", "--height", "480"] + .map(String::from) + .to_vec(); + append_profile_game_arguments( + &mut args, + Some(true), + Some("--tracy --tracyNoImages --width 1920 --fullscreen"), + ); + assert_eq!( + args, + vec![ + "--username", + "Steve", + "--height", + "480", + "--tracy", + "--tracyNoImages", + "--width", + "1920", + "--fullscreen" + ] + ); + } + #[test] fn an_empty_quoted_value_survives() { assert_eq!(split_custom_args(r#"-Dempty="""#), vec!["-Dempty="]);