diff --git a/crates/app-iced/src/app/tests.rs b/crates/app-iced/src/app/tests.rs index ce40508..2f10ec5 100644 --- a/crates/app-iced/src/app/tests.rs +++ b/crates/app-iced/src/app/tests.rs @@ -12,6 +12,8 @@ use unlucky_presenter::settings_bridge::SettingsBridge; #[cfg(test)] mod appearance; #[cfg(test)] +mod instances_save_loop; +#[cfg(test)] mod presenter; use super::message::Message; diff --git a/crates/app-iced/src/app/tests/instances_save_loop.rs b/crates/app-iced/src/app/tests/instances_save_loop.rs new file mode 100644 index 0000000..1a350af --- /dev/null +++ b/crates/app-iced/src/app/tests/instances_save_loop.rs @@ -0,0 +1,145 @@ +//! 「启动参数保存失败」的**整环**回归(M157 r4 复审 P3-a)。 +//! +//! 仓里原来只有这条环的两半:`LaunchForm::sync` 的单测(本 crate 的 `pages/instances/state.rs`) +//! 与「Save 只派发不清脏」(`app/update/instances.rs`)。两半各自绿,但**接线**(消息 → +//! `dispatch` → 后端回事件 → `pump` → `refresh_view` → `sync_launch_form`)没有任何活着的断言。 +//! 把 `dirty = false` 提前到派发前,只有这条整环测试会红(单测两半都不会)。 +//! +//! 造的后端与真 host 的失败路径同形:收到 `SetInstanceLaunchArgs` 只回一条 Toast(错误的用户 +//! 可见反馈),**不回** `InstancesChanged`——盘上没改,快照还是旧的。 + +use std::cell::RefCell; +use std::rc::Rc; + +use unlucky_design::color::SchemeFlavor; +use unlucky_presenter::dto::{ + AppCommand, AppEvent, Backend, CoreEvent, InstanceRuntime, InstanceSummary, LaunchArgsView, + LoaderInfo, LoaderKind, +}; +use unlucky_presenter::router::Route; +use unlucky_presenter::settings_bridge::SettingsBridge; +use unlucky_presenter::toast::{Toast, ToastKind}; + +use crate::app::message::Message; +use crate::app::state::{App, TICK}; +use crate::cli::Options; +use crate::pages; +use crate::theme::Families; + +const FAMILIES: Families = Families { + text: "Segoe UI", + display: "Segoe UI", + mono: "Consolas", +}; + +/// 失败路径的后端:命令进台账;`SetInstanceLaunchArgs` 只回一条错误 Toast。 +#[derive(Debug, Default)] +struct FailingSaveBackend { + dispatched: Rc>>, + queued: Vec, +} + +impl Backend for FailingSaveBackend { + fn drain_events(&mut self) -> Vec { + std::mem::take(&mut self.queued) + } + + fn dispatch(&mut self, command: AppCommand) { + self.dispatched.borrow_mut().push(command.clone()); + if matches!(command, AppCommand::SetInstanceLaunchArgs { .. }) { + // 真 host 在 `dispatch_inner` 的 Err 分支只发 toast(不发 InstancesChanged)。 + self.queued.push(AppEvent::Toast(Toast { + id: 0, + kind: ToastKind::Error, + text: "内存超出范围".to_string(), + remaining_ms: 0, + })); + } + } +} + +/// 盘上的实例快照:内存 4096、其余留空(= 保存失败时 pump 会带回来的那份)。 +fn snapshot() -> Vec { + vec![InstanceSummary { + id: "survival".to_string(), + name: "生存".to_string(), + mc_version: "1.20.4".to_string(), + loader: LoaderInfo { + kind: LoaderKind::Vanilla, + version: String::new(), + }, + last_played: None, + runtime: InstanceRuntime::Idle, + icon: None, + isolation: "isolated".to_string(), + version_id: "1.20.4".to_string(), + version_present: true, + launch: LaunchArgsView { + instance_id: "survival".to_string(), + memory_mb: Some(4096), + memory_effective_mb: 4096, + ..LaunchArgsView::default() + }, + }] +} + +fn app_with_snapshot() -> (App, Rc>>) { + let dispatched = Rc::new(RefCell::new(Vec::new())); + let backend = FailingSaveBackend { + dispatched: Rc::clone(&dispatched), + queued: vec![AppEvent::Core(CoreEvent::InstancesChanged(snapshot()))], + }; + let app = App::with_presenter( + Options::default(), + FAMILIES, + SchemeFlavor::Light, + Box::new(backend), + SettingsBridge::memory_only(), + ); + (app, dispatched) +} + +/// **保存失败不许吃掉草稿**(整环):输入 `4g` → 点保存 → 后端回错误事件 → pump 重建视图。 +/// +/// 抓得住:①`Save` 在派发前清脏(草稿被旧快照覆盖成 4096);②`LaunchForm::sync` 去掉 +/// `matches` 判据(同样覆盖);③`pump` 没把事件喂进 `apply`/`refresh_view`(表单不会被播种, +/// 下面的前置断言会红)。 +/// 抓不住:真 host 的落盘失败(那是 `host::launch_args` 的单测射程)。 +#[test] +fn failed_save_keeps_the_draft_through_the_whole_loop() { + let (mut app, dispatched) = app_with_snapshot(); + + // 首屏快照到位 → 自动选中 → 表单按盘上的值播种。 + let _ = app.update(Message::Boot); + let _ = app.update(Message::Page(pages::Message::Navigate(Route::Detail( + "survival".to_string(), + )))); + app.advance(TICK); + assert_eq!(app.launch_form.memory, "4096", "首帧要按快照播种"); + assert!(!app.launch_form.dirty); + + // 用户把内存改成非法值(host 会拒),然后点保存。 + let _ = app.update(Message::Page(pages::Message::Instances( + pages::InstanceMessage::Memory("4g".to_string()), + ))); + assert_eq!(app.launch_form.memory, "4g"); + let _ = app.update(Message::Page(pages::Message::Instances( + pages::InstanceMessage::Save, + ))); + assert!( + dispatched + .borrow() + .iter() + .any(|command| matches!(command, AppCommand::SetInstanceLaunchArgs { .. })), + "保存要把命令派发出去" + ); + + // 失败回程:一条错误 Toast → pump → refresh_view → sync_launch_form(旧快照)。 + app.advance(TICK); + + assert_eq!( + app.launch_form.memory, "4g", + "保存失败后,pump 用旧快照把用户刚敲的内容冲掉了" + ); + assert!(app.launch_form.dirty, "草稿还在,脏标记也不能掉"); +} diff --git a/crates/app-iced/src/pages/instances/launch_args.rs b/crates/app-iced/src/pages/instances/launch_args.rs index 6774f60..8f64e3d 100644 --- a/crates/app-iced/src/pages/instances/launch_args.rs +++ b/crates/app-iced/src/pages/instances/launch_args.rs @@ -196,7 +196,7 @@ fn backend_row<'a>( ui, TypeRole::BodySmall, false, - texts.launch_backend_gated(&facts.mc_version), + texts.launch_backend_gated(&facts.mc_version, &facts.min_version), ui.color(Role::Error), )); } diff --git a/crates/app-iced/src/pages/instances/state.rs b/crates/app-iced/src/pages/instances/state.rs index 0c0951e..1343e31 100644 --- a/crates/app-iced/src/pages/instances/state.rs +++ b/crates/app-iced/src/pages/instances/state.rs @@ -87,17 +87,30 @@ impl LaunchForm { /// 表单当前内容是否与一份快照(= 盘上已持久化的值)完全一致。 /// - /// 比较的是**文本口径**:内存按「快照有值 → 十进制串,无值 → 空串」对齐,其余直接比原文。 + /// **内存按数值比**:host 会把 `"04096"` / `"+4096"` 归一成 `4096` 落盘,回程快照给的是 + /// `Some(4096)`——按文本比会永远不等,于是表单一直脏、也不再跟随外部改动(r4 复审 P3-b)。 + /// 解析失败(用户正在敲半截)才退回文本口径。 + /// 其余字段直接比原文(JVM/游戏参数由 core 的 `tokenize` 消费,不做归一)。 #[must_use] pub fn matches(&self, view: &LaunchArgsView) -> bool { - let memory = view.memory_mb.map(|mb| mb.to_string()).unwrap_or_default(); self.jvm.trim() == view.jvm_args.trim() && self.game.trim() == view.game_args.trim() - && self.memory.trim() == memory + && self.memory_matches(view.memory_mb) && self.java.trim() == view.java_path.trim() && self.backend == view.graphics_backend } + /// 内存字段是否等于快照里的值(数值优先,解析失败退文本)。 + fn memory_matches(&self, persisted: Option) -> bool { + let typed = self.memory.trim(); + match (typed.parse::(), persisted) { + (Ok(typed), Some(persisted)) => typed == persisted, + // 空输入 = 跟随全局:快照那边也应当是「没有实例覆盖」。 + (Err(_), None) => typed.is_empty(), + _ => false, + } + } + /// 提交补丁:**五项全给**(表单是整份可见的,不用增量语义会让「清空」无法表达)。 #[must_use] pub fn patch(&self) -> LaunchArgsPatch { @@ -229,6 +242,34 @@ mod tests { assert_eq!(form.jvm, "-Xss4m"); } + /// **内存按数值比**(r4 复审 P3-b):`04096` / `+4096` 落盘后回程是 `4096`,必须判等。 + /// + /// 抓得住:`matches` 退回文本比较——那样这两类输入会永远「没保存成功」,表单一直脏、 + /// 之后外部改配置也不再重播种。 + #[test] + fn memory_is_compared_by_value_not_by_text() { + for typed in ["04096", "+4096", " 4096 "] { + let mut form = LaunchForm::from_view(Some("a"), &view("a", "", Some(4096), "default")); + form.memory = typed.to_string(); + form.dirty = true; + assert!( + form.matches(&view("a", "", Some(4096), "default")), + "`{typed}` 应判等" + ); + form.sync(&view("a", "", Some(4096), "default")); + assert!(!form.dirty, "`{typed}` 保存确认后要清脏"); + } + // 数值不等就是不等于;半截输入(解析失败)也不许判等。 + let mut form = LaunchForm::from_view(Some("a"), &view("a", "", Some(4096), "default")); + form.memory = "8".to_string(); + assert!(!form.matches(&view("a", "", Some(4096), "default"))); + form.memory = "4g".to_string(); + assert!(!form.matches(&view("a", "", Some(4096), "default"))); + // 清空 = 跟随全局:快照也得是「没有实例覆盖」。 + form.memory.clear(); + assert!(form.matches(&view("a", "", None, "default"))); + } + /// 补丁五项全给:空串就是「清空」,不留 `None` 让 host 猜。 #[test] fn patch_always_covers_all_five_fields() { diff --git a/crates/app-iced/src/pages/instances/tests/i18n.rs b/crates/app-iced/src/pages/instances/tests/i18n.rs index 8973176..1d257c0 100644 --- a/crates/app-iced/src/pages/instances/tests/i18n.rs +++ b/crates/app-iced/src/pages/instances/tests/i18n.rs @@ -61,7 +61,7 @@ const PAGE_SOURCES: [(&str, &str); 6] = [ /// | `isolated` / `shared` | core 的隔离档位**稳定 id**(`IsolationMode::as_str`),`isolation()` 只认这两档,其余原样上屏 | /// | `pid` / `reason` | `Catalog::render` 的**占位符名**(与 catalog 里的 `{pid}` / `{reason}` 对应) | /// | `Instances` | `Texts::keys()` 的**族名前缀**(`Key::ALL` 的过滤条件,不是文案) | -const ALLOWED_LITERALS: [&str; 18] = [ +const ALLOWED_LITERALS: [&str; 19] = [ "{date} {time} UTC", "isolated", "shared", @@ -76,6 +76,8 @@ const ALLOWED_LITERALS: [&str; 18] = [ // M157:`Catalog::render` 的占位符名(`{version}` / `{value}` / `{default}`),不是文案。 "version", "value", + // M157 r4:门槛文案的占位符名(版本号由 DTO 给,目录里不写死)。 + "min", // M157:`instances/demo.rs` 的**出图夹具**(用户数据的形状,不是界面文案): // 一个示例 JVM 参数、两个示例 MC 版本、一个示例 Java 路径,以及夹具开关的环境变量名与取值。 "-XX:+UseG1GC", diff --git a/crates/app-iced/src/pages/instances/text.rs b/crates/app-iced/src/pages/instances/text.rs index c49f935..69874cb 100644 --- a/crates/app-iced/src/pages/instances/text.rs +++ b/crates/app-iced/src/pages/instances/text.rs @@ -120,10 +120,15 @@ impl Texts { pub(super) fn launch_backend(self) -> &'static str { self.text(Key::InstancesLaunchBackend) } - /// 「需游戏版本 26.2 及以上(本实例 1.20.4)」。 - pub(super) fn launch_backend_gated(self, version: &str) -> String { - self.0 - .render(Key::InstancesLaunchBackendGated, &[("version", version)]) + /// 「需游戏版本 {min} 及以上(本实例 {version})」。 + /// + /// `min` 来自 DTO(`LaunchFacts::min_version` ← core 的 `GRAPHICS_BACKEND_MIN_VERSION`), + /// **不在目录里硬编码版本号**——否则 core 一改门槛,文案会继续骗人(r4 复审 P3-c)。 + pub(super) fn launch_backend_gated(self, version: &str, min: &str) -> String { + self.0.render( + Key::InstancesLaunchBackendGated, + &[("version", version), ("min", min)], + ) } /// 「生效:vulkan」。 pub(super) fn launch_effective(self, value: &str) -> String { diff --git a/crates/cli/src/commands/instance_launch.rs b/crates/cli/src/commands/instance_launch.rs index 19d88d4..22e5502 100644 --- a/crates/cli/src/commands/instance_launch.rs +++ b/crates/cli/src/commands/instance_launch.rs @@ -183,7 +183,7 @@ fn patch_from(args: &InstanceLaunchArgs) -> LaunchOverridesPatch { /// 全局层(三个设置键)。 pub(crate) fn global_defaults(ctx: &Ctx) -> GlobalLaunchDefaults { - let store = SettingStore::load(&ctx.launcher_config(), launcher_schema()); + let store = SettingStore::load(ctx.launcher_config(), launcher_schema()); let text = |key: &str| -> Option { store .doc() diff --git a/crates/core/src/launch/overrides/tests.rs b/crates/core/src/launch/overrides/tests.rs index 590f50f..7dc1f58 100644 --- a/crates/core/src/launch/overrides/tests.rs +++ b/crates/core/src/launch/overrides/tests.rs @@ -295,59 +295,62 @@ fn backend_allowed_combines_capability_and_threshold() { // 周快照:看库表。 assert!(backend_allowed(&GameVersion::parse("26w14a"), &with_vulkan)); assert!(!backend_allowed(&GameVersion::parse("26w14a"), &without)); +} - /// **周快照 + 加载器**(M157 r3 复审 P2-1):装配门限必须按**继承链的 MC 根版本**判。 - /// - /// 造一个 Fabric 实例(id `26w14a-fabric-0.16.5`,继承周快照 `26w14a`),库表里有 - /// `org.lwjgl:lwjgl-vulkan`:`game_version = Some("26w14a")` ⇒ 命令行带 `--graphicsBackend vulkan`; - /// 退回到「拿带加载器后缀的 version_id 当游戏版本」⇒ `GameVersion::is_weekly_snapshot` - /// 的 6 字符形态认不出来,参数被跳过(**负向锚点**:这正是复审复现出来的静默丢失)。 - /// - /// 抓得住:adapter 的 `game_version` 传错(调用方又回退成 `None`)——周快照 loader 实例 - /// 「UI 说可用、命令行里没有」。 - /// 抓不住:两个调用点自己有没有传(那由 presenter `host::launch::tests` 与 - /// CLI `snapshot::tests` 各自的根版本断言守)。 - #[test] - fn modded_weekly_snapshot_needs_the_root_version_for_the_gate() { - let version_id = "26w14a-fabric-0.16.5"; - let libraries = [LibraryInput::new( - "C:/mc/libraries/org/lwjgl/lwjgl-vulkan/3.4.3/lwjgl-vulkan-3.4.3.jar", - "org.lwjgl:lwjgl-vulkan:3.4.3", - )]; +/// **周快照 + 加载器**(M157 r3 复审 P2-1):装配门限必须按**继承链的 MC 根版本**判。 +/// +/// 造一个 Fabric 实例(id `26w14a-fabric-0.16.5`,继承周快照 `26w14a`),库表里有 +/// `org.lwjgl:lwjgl-vulkan`:`game_version = Some("26w14a")` ⇒ 命令行带 `--graphicsBackend vulkan`; +/// 退回到「拿带加载器后缀的 version_id 当游戏版本」⇒ `GameVersion::is_weekly_snapshot` +/// 的 6 字符形态认不出来,参数被跳过(**负向锚点**:这正是复审复现出来的静默丢失)。 +/// +/// 抓得住:adapter 的 `game_version` 传错(调用方又回退成 `None`)——周快照 loader 实例 +/// 「UI 说可用、命令行里没有」。 +/// 抓不住:两个调用点自己有没有传(那由 presenter `host::launch::tests` 与 +/// CLI `snapshot::tests` 各自的根版本断言守)。 +#[test] +fn modded_weekly_snapshot_needs_the_root_version_for_the_gate() { + let version_id = "26w14a-fabric-0.16.5"; + let libraries = [LibraryInput::new( + "C:/mc/libraries/org/lwjgl/lwjgl-vulkan/3.4.3/lwjgl-vulkan-3.4.3.jar", + "org.lwjgl:lwjgl-vulkan:3.4.3", + )]; - for (game_version, expected) in [(Some("26w14a"), true), (None, false)] { - let snapshot = SnapshotInput { - game_version, - libraries: &libraries, - primary_jar: PathBuf::from("C:/mc/versions/x/x.jar"), - ..SnapshotInput::minimal(version_id, "net.minecraft.client.main.Main") - }; - let options = AdapterOptions { - check_classpath: false, - language: "zh-CN".to_string(), - ..AdapterOptions::default() - }; - let mut out = build_request(&snapshot, options).request; - let resolved = resolve( - Some(&LaunchOverrides { - graphics_backend: Some(GraphicsBackend::Vulkan), - ..LaunchOverrides::default() - }), - &GlobalLaunchDefaults::default(), - ); - apply(&mut out, &resolved); - let command = assemble(&out, &ctx()).expect("组装应成功"); - let sent = command.args.iter().any(|arg| arg == "--graphicsBackend"); - assert_eq!( - sent, expected, - "game_version={game_version:?} 时是否下发不对:{:?}", - command.args - ); - let skipped = command - .warnings - .iter() - .any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })); - assert_eq!(skipped, !expected, "跳过与否必须与下发相反(不静默)"); - } + for (game_version, expected) in [(Some("26w14a"), true), (None, false)] { + let snapshot = SnapshotInput { + game_version, + libraries: &libraries, + primary_jar: PathBuf::from("C:/mc/versions/x/x.jar"), + ..SnapshotInput::minimal(version_id, "net.minecraft.client.main.Main") + }; + let options = AdapterOptions { + // `assemble` 要求 Java 可执行文件非空(`EmptyJavaPath`);adapter 默认是空路径, + // 所以这里显式给一个(原来这条用例从没被执行过,r4 把它提到模块层才暴露出来)。 + java: JavaSelection::new("C:/java/bin/java.exe", 21, 64), + check_classpath: false, + language: "zh-CN".to_string(), + ..AdapterOptions::default() + }; + let mut out = build_request(&snapshot, options).request; + let resolved = resolve( + Some(&LaunchOverrides { + graphics_backend: Some(GraphicsBackend::Vulkan), + ..LaunchOverrides::default() + }), + &GlobalLaunchDefaults::default(), + ); + apply(&mut out, &resolved); + let command = assemble(&out, &ctx()).expect("组装应成功"); + let sent = command.args.iter().any(|arg| arg == "--graphicsBackend"); + assert_eq!( + sent, expected, + "game_version={game_version:?} 时是否下发不对:{:?}", + command.args + ); + let skipped = command + .warnings + .iter() + .any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })); + assert_eq!(skipped, !expected, "跳过与否必须与下发相反(不静默)"); } } diff --git a/crates/i18n/src/catalog/golden.rs b/crates/i18n/src/catalog/golden.rs index 15bf155..a1cb711 100644 --- a/crates/i18n/src/catalog/golden.rs +++ b/crates/i18n/src/catalog/golden.rs @@ -50,7 +50,7 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[ ("InstancesLaunchJava", "Java executable", "Java 可执行文件"), ("InstancesLaunchJavaHint", "Empty = follow the global setting, then auto-pick (managed > JAVA_HOME > PATH).", "留空 = 跟随全局设置,再退到自动挑选(托管 > 环境变量 > 系统路径)。"), ("InstancesLaunchBackend", "Graphics backend", "图形后端"), - ("InstancesLaunchBackendGated", "Requires MC 26.2+ (this instance: {version}). The flag is not sent on this version.", "需游戏版本 26.2 及以上(本实例 {version}),这个版本不会下发该参数。"), + ("InstancesLaunchBackendGated", "Requires MC {min}+ (this instance: {version}). The flag is not sent on this version.", "需游戏版本 {min} 及以上(本实例 {version}),这个版本不会下发该参数。"), ("InstancesLaunchEffective", "Effective: {value}", "生效:{value}"), ("InstancesBackendDefault", "Default", "默认"), ("InstancesBackendOpengl", "OpenGL", "OpenGL 后端"), diff --git a/crates/i18n/src/catalog/instances.rs b/crates/i18n/src/catalog/instances.rs index 6ed82f9..41abc7a 100644 --- a/crates/i18n/src/catalog/instances.rs +++ b/crates/i18n/src/catalog/instances.rs @@ -89,7 +89,7 @@ macro_rules! instances_entries { InstancesLaunchJava => ("Java executable", "Java 可执行文件"), InstancesLaunchJavaHint => ("Empty = follow the global setting, then auto-pick (managed > JAVA_HOME > PATH).", "留空 = 跟随全局设置,再退到自动挑选(托管 > 环境变量 > 系统路径)。"), InstancesLaunchBackend => ("Graphics backend", "图形后端"), - InstancesLaunchBackendGated => ("Requires MC 26.2+ (this instance: {version}). The flag is not sent on this version.", "需游戏版本 26.2 及以上(本实例 {version}),这个版本不会下发该参数。"), + InstancesLaunchBackendGated => ("Requires MC {min}+ (this instance: {version}). The flag is not sent on this version.", "需游戏版本 {min} 及以上(本实例 {version}),这个版本不会下发该参数。"), InstancesLaunchEffective => ("Effective: {value}", "生效:{value}"), InstancesBackendDefault => ("Default", "默认"), InstancesBackendOpengl => ("OpenGL", "OpenGL 后端"), diff --git a/crates/presenter/src/mock_data.rs b/crates/presenter/src/mock_data.rs index db91faf..5591b8c 100644 --- a/crates/presenter/src/mock_data.rs +++ b/crates/presenter/src/mock_data.rs @@ -210,6 +210,29 @@ pub fn mock_versions_for(category: VersionCategory) -> Vec { .collect() } +/// 演示用的启动参数视图(M157):一条真实形状的实例(有 JVM 参数、Vulkan、门槛放行)。 +fn mock_launch() -> LaunchArgsView { + LaunchArgsView { + instance_id: "survival".to_string(), + jvm_args: "-XX:+UseG1GC".to_string(), + game_args: String::new(), + memory_mb: Some(4096), + memory_effective_mb: 4096, + java_path: String::new(), + java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(), + graphics_backend: "vulkan".to_string(), + graphics_backend_effective: "vulkan".to_string(), + graphics_backend_allowed: true, + mc_version: "26.2-snapshot-2".to_string(), + graphics_backend_min_version: "26.2-snapshot-2".to_string(), + graphics_backend_options: vec![ + "default".to_string(), + "opengl".to_string(), + "vulkan".to_string(), + ], + } +} + #[cfg(test)] mod tests { use crate::dto::{AncientKind, VersionCategory, VersionKind}; @@ -240,26 +263,3 @@ mod tests { assert!(mock_versions_for(VersionCategory::Mod).is_empty()); } } - -/// 演示用的启动参数视图(M157):一条真实形状的实例(有 JVM 参数、Vulkan、门槛放行)。 -fn mock_launch() -> LaunchArgsView { - LaunchArgsView { - instance_id: "survival".to_string(), - jvm_args: "-XX:+UseG1GC".to_string(), - game_args: String::new(), - memory_mb: Some(4096), - memory_effective_mb: 4096, - java_path: String::new(), - java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(), - graphics_backend: "vulkan".to_string(), - graphics_backend_effective: "vulkan".to_string(), - graphics_backend_allowed: true, - mc_version: "26.2-snapshot-2".to_string(), - graphics_backend_min_version: "26.2-snapshot-2".to_string(), - graphics_backend_options: vec![ - "default".to_string(), - "opengl".to_string(), - "vulkan".to_string(), - ], - } -}