From 6344d2e985be811a34f966665c47dd1973388d9c Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:28:16 +0200 Subject: [PATCH] feat(ui): JVM arguments setting --- .../src/view/app/cluster/cluster_settings.rs | 10 +++ .../src/view/app/settings/minecraft.rs | 16 ++++ packages/oneclient_mc/src/arguments.rs | 75 ++++++++++++++++++- 3 files changed, 97 insertions(+), 4 deletions(-) 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 3936d29e..7190ac43 100644 --- a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs +++ b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs @@ -79,6 +79,7 @@ impl Component for ClusterSettings { } .into_element(), ) + .child(text_row(cluster_id, TextField::JvmArgs, &profile, &global)) .child(section_header("GAME")) .child( ToggleRow { @@ -420,6 +421,7 @@ impl Component for ResolutionRow { #[derive(Clone, Copy, PartialEq)] enum TextField { + JvmArgs, Pre, Wrapper, Post, @@ -428,6 +430,12 @@ enum TextField { impl TextField { fn meta(self) -> (IconType, &'static str, &'static str, &'static str) { match self { + Self::JvmArgs => ( + IconType::Terminal, + "JVM Arguments", + "Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.", + "-XX:+UseG1GC", + ), Self::Pre => ( IconType::FilePlus02, "Pre-Launch Command", @@ -451,6 +459,7 @@ impl TextField { fn value(self, profile: &GameSettingsProfile) -> Option { match self { + Self::JvmArgs => profile.launch_args.clone(), Self::Pre => profile.hook_pre.clone(), Self::Wrapper => profile.hook_wrapper.clone(), Self::Post => profile.hook_post.clone(), @@ -460,6 +469,7 @@ impl TextField { fn patch(self, value: Patch) -> ProfileUpdate { let mut u = ProfileUpdate::default(); match self { + Self::JvmArgs => u.launch_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 914361a6..e4797bdb 100644 --- a/packages/oneclient_app/src/view/app/settings/minecraft.rs +++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs @@ -38,6 +38,10 @@ impl Component for SettingsMinecraft { let v = profile.mem_max.map(|m| m.to_string()).unwrap_or_default(); move || v }); + let jvm_args = use_state({ + let v = profile.launch_args.clone().unwrap_or_default(); + move || v + }); let pre_launch_command = use_state({ let v = profile.hook_pre.clone().unwrap_or_default(); move || v @@ -58,6 +62,7 @@ impl Component for SettingsMinecraft { &width.read(), &height.read(), &memory.read(), + &jvm_args.read(), &pre_launch_command.read(), &wrapper_command.read(), &post_exit_command.read(), @@ -89,6 +94,14 @@ impl Component for SettingsMinecraft { "The amount of memory in megabytes allocated for the game.", memory_field(memory), )) + .child(settings_row( + IconType::Terminal, + "JVM Arguments", + "Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.", + TextInput::new(jvm_args) + .placeholder("-XX:+UseG1GC") + .width(Size::px(220.)), + )) .child(section_header("PROCESS")) .child(settings_row( IconType::FilePlus02, @@ -118,11 +131,13 @@ impl Component for SettingsMinecraft { } } +#[allow(clippy::too_many_arguments)] fn build_update( fullscreen: bool, width: &str, height: &str, memory: &str, + jvm_args: &str, pre: &str, wrapper: &str, post: &str, @@ -144,6 +159,7 @@ fn build_update( force_fullscreen: Patch::Set(fullscreen), resolution, mem_max, + launch_args: command_patch(jvm_args), hook_pre: command_patch(pre), hook_wrapper: command_patch(wrapper), hook_post: command_patch(post), diff --git a/packages/oneclient_mc/src/arguments.rs b/packages/oneclient_mc/src/arguments.rs index 498e9cd3..f8efd7e6 100644 --- a/packages/oneclient_mc/src/arguments.rs +++ b/packages/oneclient_mc/src/arguments.rs @@ -52,14 +52,49 @@ pub fn java_arguments( } parsed.push(format!("-Xmx{mem_max}M")); - - if !custom_args.is_empty() { - parsed.push(custom_args); - } + parsed.extend(split_custom_args(&custom_args)); Ok(parsed) } +/// Splits a user-entered JVM argument string into individual arguments. +/// +/// Whitespace-separated, with double quotes for values that contain spaces +/// (`-Dfoo="a b"`), which is what people expect from every other launcher. +/// Pushing the whole string as one argv entry, as this used to, means the JVM +/// sees a single unrecognised option as soon as there is more than one. +fn split_custom_args(raw: &str) -> Vec { + let mut args = Vec::new(); + let mut current = String::new(); + let mut quoted = false; + let mut started = false; + + for ch in raw.chars() { + match ch { + '"' => { + quoted = !quoted; + started = true; + } + c if c.is_whitespace() && !quoted => { + if started { + args.push(std::mem::take(&mut current)); + started = false; + } + } + c => { + current.push(c); + started = true; + } + } + } + + if started { + args.push(current); + } + + args +} + #[allow(clippy::too_many_arguments)] pub fn minecraft_arguments( version_updated: bool, @@ -415,3 +450,35 @@ where Ok(()) } + +#[cfg(test)] +mod tests { + use super::split_custom_args; + + #[test] + fn blank_input_contributes_nothing() { + assert!(split_custom_args("").is_empty()); + assert!(split_custom_args(" \t ").is_empty()); + } + + #[test] + fn each_flag_becomes_its_own_argument() { + assert_eq!( + split_custom_args("-XX:+UseG1GC -Xms2G"), + vec!["-XX:+UseG1GC", "-Xms2G"] + ); + } + + #[test] + fn quotes_hold_a_value_with_spaces_together() { + assert_eq!( + split_custom_args(r#"-Dname="My Server" -Xss1M"#), + vec!["-Dname=My Server", "-Xss1M"] + ); + } + + #[test] + fn an_empty_quoted_value_survives() { + assert_eq!(split_custom_args(r#"-Dempty="""#), vec!["-Dempty="]); + } +}