feat(launch): M157 实例级启动参数(五项回落 + graphicsBackend 三态)

core: 新增 game/launch_overrides(LaunchOverrides 五项 All-Option + 三级回落 + 文本补丁校验)与 launch/overrides(拼装落位:全局 java.extra_args 进 user.jvm、实例 jvmArgs 进 override_jvm、gameArgs 进 user.game、memory 进 -Xmx、backend 进 --graphicsBackend 并复用既有版本门槛告警);实例 schema 登记 launch.* 五键。

presenter: dto/LaunchArgsView+LaunchArgsPatch,主机侧读/写与门槛判定(backend_allowed),AppCommand::SetInstanceLaunchArgs。

cli: 新命令 instances launch-args(读/写/--clear),launch 装配链消费回落值。

app-iced: 实例详情页「启动参数」区(JVM/游戏参数、内存、Java、三态后端 chips + 灰化提示 + 保存/恢复),表单状态归 App、i18n 中英文案同步。
This commit is contained in:
AskaEth committed 2026-11-07 10:36:35 +08:00
1 parent d68523b02a
commit e2021645d7
56 files changed
+3776 -607

No files matched your search

+96
View File
@@ -41,6 +41,11 @@ pub const ISOLATION_OVERRIDES: &[&str] = &["inherit", "isolated", "shared", "cus
pub const ISOLATION_RESOLVED: &[&str] = &["isolated", "shared", "custom"];
/// 实例可覆盖的设置键(enum 白名单,拼错即校验失败而不是静默不生效)。
pub const OVERRIDE_KEYS: &[&str] = &["maxMemory", "javaType", "runningDirectory"];
/// `launch.graphicsBackend` 的允许值(`default` = 不下发 `--graphicsBackend`)。
///
/// 与 `crate::game::launch_overrides::GraphicsBackend::IDS` 必须是同一份集合——setting 层
/// 不依赖 game 层(分层纪律),一致性由本文件的 `graphics_backends_match_the_game_layer` 固定。
pub const GRAPHICS_BACKENDS: &[&str] = &["default", "opengl", "vulkan"];
/// profiles.json 当前 schema 版本。
#[must_use]
@@ -111,6 +116,42 @@ pub fn instance_schema() -> SettingSchema {
AiPolicy::locked(),
"该实例覆盖了全局哪些设置键(enum 白名单,拼错即校验失败)",
))
// M157:实例级启动参数(`launch` 段,五项全可选、逐项回落到全局)。
// 作者是 `game::install::instance::InstanceConfig`(serde 形状与这些路径逐键对应),
// 回落规则与生效值在 `game::launch_overrides`,拼装消费在 `launch::overrides`。
.with(SettingSpec::text(
"launch.jvmArgs",
"",
1024,
AiPolicy::sensitive(),
"实例级 JVM 参数(空白分隔、支持引号);留空 = 只用全局 java.extra_args(两者叠加,实例优先)",
))
.with(SettingSpec::text(
"launch.gameArgs",
"",
1024,
AiPolicy::audited(),
"实例级游戏参数(追加在命令行末段,可覆盖前面一切游戏参数);留空 = 不追加",
))
.with(SettingSpec::int(
"launch.memoryMb",
0,
(0, 65536),
AiPolicy::sensitive(),
"实例级最大堆(MiB);0/缺键 = 跟随全局 java.default_memory_mb(有效区间 256~65536)",
))
.with(SettingSpec::path(
"launch.javaPath",
AiPolicy::sensitive(),
"实例级 Java 可执行文件;留空 = 跟随全局 java.selected_path 再退到自动挑(托管 > JAVA_HOME > PATH)",
))
.with(SettingSpec::enumeration(
"launch.graphicsBackend",
"default",
GRAPHICS_BACKENDS,
AiPolicy::audited(),
"图形后端:default(不下发)/ opengl / vulkan。只对 MC 26.2-snapshot-2+ 下发,老版本会被门槛拦下并给出诊断告警",
))
.with(SettingSpec::text(
"lastModeChange.at",
"",
@@ -153,3 +194,58 @@ pub fn instance_schema() -> SettingSchema {
"该次切换的提示是否已展示过(保证切换后首次启动只提示一次)",
))
}
#[cfg(test)]
mod tests {
use super::*;
/// M157:图形后端词表**只有一份真相源**(`game::launch_overrides::GraphicsBackend`),
/// setting 层是它的白名单视图——两边脱节就是「写得进、读不出」或「UI 三态少一档」。
#[test]
fn graphics_backends_match_the_game_layer() {
use crate::game::launch_overrides::GraphicsBackend;
let mut setting: Vec<&str> = GRAPHICS_BACKENDS.to_vec();
let mut game: Vec<&str> = GraphicsBackend::IDS.to_vec();
setting.sort_unstable();
game.sort_unstable();
assert_eq!(setting, game, "setting 白名单与 game 层词表脱节了");
for id in GRAPHICS_BACKENDS {
assert!(
GraphicsBackend::parse(id).is_some(),
"`{id}` 在 game 层解析不出来"
);
}
}
/// 五项键真的进了实例 schema 白名单(且 `default` 之外的后端值被拒)。
#[test]
fn launch_section_keys_are_registered() {
let schema = instance_schema();
assert!(schema
.validate("launch.jvmArgs", &serde_json::json!("-XX:+UseG1GC"))
.is_ok());
assert!(schema
.validate("launch.gameArgs", &serde_json::json!("--fullscreen"))
.is_ok());
assert!(schema
.validate("launch.memoryMb", &serde_json::json!(4096))
.is_ok());
assert!(schema
.validate(
"launch.javaPath",
&serde_json::json!("D:/jdk21/bin/java.exe")
)
.is_ok());
assert!(schema
.validate("launch.graphicsBackend", &serde_json::json!("vulkan"))
.is_ok());
// 非法后端值:白名单外必须报错(手改文件绕不过校验)。
assert!(schema
.validate("launch.graphicsBackend", &serde_json::json!("vulkan2"))
.is_err());
// 越界内存:schema 层也拦。
assert!(schema
.validate("launch.memoryMb", &serde_json::json!(1_000_000))
.is_err());
}
}