Add custom game args to settings (#905)

This commit is contained in:
Julian Chang 2026-10-05 17:21:05 -04:00 • committed by GitHub
parent e7ddd85960
commit d073e10a3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 171 additions and 56 deletions

View file

@ -0,0 +1,5 @@
---
default: minor
---
# Game settings have a Game Arguments field for passing extra arguments to Minecraft, globally or per cluster

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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<String> {
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,

View file

@ -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),

View file

@ -12,6 +12,7 @@ pub struct GameSettingsProfile {
pub force_fullscreen: Option<bool>,
pub mem_max: Option<u32>,
pub launch_args: Option<String>,
pub game_args: Option<String>,
pub launch_env: Option<String>,
pub hook_pre: Option<String>,
pub hook_wrapper: Option<String>,
@ -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(),

View file

@ -115,6 +115,7 @@ pub struct ProfileUpdate {
pub force_fullscreen: Patch<bool>,
pub mem_max: Patch<u32>,
pub launch_args: Patch<String>,
pub game_args: Patch<String>,
pub launch_env: Patch<String>,
pub hook_pre: Patch<String>,
pub hook_wrapper: Patch<String>,
@ -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);

View file

@ -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

View file

@ -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;

View file

@ -14,7 +14,7 @@ pub async fn list_all(pool: &SqlitePool) -> DbResult<Vec<SettingProfileRow>> {
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<Option<Setti
let row = 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
WHERE name = ?
@ -58,23 +58,24 @@ pub async fn upsert(
SettingProfileRow,
r#"
INSERT INTO setting_profiles (
name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,
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
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
java_path = excluded.java_path,
resolution = excluded.resolution,
force_fullscreen = excluded.force_fullscreen,
mem_max = excluded.mem_max,
launch_args = excluded.launch_args,
game_args = excluded.game_args,
launch_env = excluded.launch_env,
hook_pre = excluded.hook_pre,
hook_wrapper = excluded.hook_wrapper,
hook_post = excluded.hook_post,
os_extra = excluded.os_extra,
browser_update_mode = excluded.browser_update_mode
RETURNING name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,
RETURNING 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
"#,
&row.name,
@ -83,6 +84,7 @@ pub async fn upsert(
row.force_fullscreen,
row.mem_max,
&row.launch_args,
&row.game_args,
&row.launch_env,
&row.hook_pre,
&row.hook_wrapper,

View file

@ -8,6 +8,7 @@ pub struct SettingProfileRow {
pub force_fullscreen: Option<i64>,
pub mem_max: Option<i64>,
pub launch_args: Option<String>,
pub game_args: Option<String>,
pub launch_env: Option<String>,
pub hook_pre: Option<String>,
pub hook_wrapper: Option<String>,

View file

@ -232,31 +232,27 @@ pub fn minecraft_arguments(
}
fn drop_repeated_arguments(args: &mut Vec<String>) {
let mut seen: HashSet<(String, Option<String>)> = HashSet::new();
let mut kept: Vec<String> = 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<String>, 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<String>) {
pub fn append_profile_game_arguments(
args: &mut Vec<String>,
force_fullscreen: Option<bool>,
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<T: AsRef<str>, 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="]);