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

+61 -1
View File
@@ -4,9 +4,11 @@
//! 两份数据源喂同一个渲染函数,这是 [`crate::pages::model`] 存在的理由。
use unlucky_i18n::{Catalog, Key, Lang};
use unlucky_presenter::dto::LaunchArgsView;
use super::super::model::{Detail, Instances, Meta, Row};
use super::super::model::{Detail, Instances, LaunchFacts, Meta, Row};
use super::data::{DEMO, NOTE, SELECTED};
use super::state::LaunchForm;
/// 演示路径的目录:**恒英文**。
///
@@ -51,6 +53,7 @@ pub(crate) fn demo() -> Instances {
status: None,
can_launch: true,
can_stop: false,
launch: launch_facts(),
};
Instances {
@@ -65,3 +68,60 @@ fn meta(label: &str, value: &str) -> Meta {
value: value.to_string(),
}
}
/// 启动参数区的演示事实(M157 的 D30 出图夹具)。
///
/// `ULL_LAUNCH_ARGS_FIXTURE=gated` 出「版本不够、三态灰化」那一屏;缺省(`open`)是
/// 「26.2 实例 + Vulkan 可选」的正常态。**这是出图夹具,不是产品逻辑**:产品路径的门槛
/// 来自 presenter 快照(core 的 `graphics_backend_allowed`),与这个环境变量无关。
pub(crate) fn launch_facts() -> LaunchFacts {
let gated = gated_fixture();
LaunchFacts {
allowed: !gated,
mc_version: if gated { "1.20.4" } else { "26.2-snapshot-2" }.to_string(),
min_version: "26.2-snapshot-2".to_string(),
memory_effective_mb: if gated { 2048 } else { 4096 },
java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(),
backend_effective: if gated { "default" } else { "vulkan" }.to_string(),
options: ["default", "opengl", "vulkan"]
.iter()
.map(|id| (*id).to_string())
.collect(),
}
}
/// 演示用的表单初值(页面状态的所有者是 `App`;harness 没有 `App`,自己持有一份)。
///
/// 返回**拥有**的表单而不是 `&'static`:`text_editor::Content` 内含 `RefCell`,不满足
/// `Sync`,做不成 `static`;harness/测试各自持有即可(一帧借用一次)。
pub(crate) fn demo_form() -> LaunchForm {
LaunchForm::from_view(Some(DEMO[SELECTED].name), &demo_view())
}
fn demo_view() -> LaunchArgsView {
let gated = gated_fixture();
LaunchArgsView {
instance_id: DEMO[SELECTED].name.to_string(),
jvm_args: "-XX:+UseG1GC".to_string(),
game_args: String::new(),
memory_mb: (!gated).then_some(4096),
memory_effective_mb: if gated { 2048 } else { 4096 },
java_path: String::new(),
java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(),
graphics_backend: if gated { "default" } else { "vulkan" }.to_string(),
graphics_backend_effective: if gated { "default" } else { "vulkan" }.to_string(),
graphics_backend_allowed: !gated,
mc_version: if gated { "1.20.4" } else { "26.2-snapshot-2" }.to_string(),
graphics_backend_min_version: "26.2-snapshot-2".to_string(),
graphics_backend_options: ["default", "opengl", "vulkan"]
.iter()
.map(|id| (*id).to_string())
.collect(),
}
}
fn gated_fixture() -> bool {
std::env::var("ULL_LAUNCH_ARGS_FIXTURE")
.map(|value| value.trim().eq_ignore_ascii_case("gated"))
.unwrap_or(false)
}
@@ -23,6 +23,7 @@ use super::super::layout::{self, FrameMetrics};
use super::super::model::Detail;
use super::super::{Action, Message};
use super::forced_state;
use super::state::LaunchForm;
use super::Texts;
// --------------------------------------------------------------- 详情列
@@ -31,6 +32,7 @@ pub(super) fn detail_column<'a>(
ui: &'a Ui,
metrics: FrameMetrics,
detail: &'a Detail,
form: &'a LaunchForm,
forced: Option<Interaction>,
texts: Texts,
) -> Element<'a, Message> {
@@ -43,6 +45,8 @@ pub(super) fn detail_column<'a>(
.width(Length::Fill);
body = push_text(ui, metrics, body, detail);
// M157:启动参数区在正文段之后、动作行之前(详情列 424px 内,卡片式)。
body = body.push(super::launch_args::section(ui, &detail.launch, form, texts));
// 页头 + 元信息网格 + **一条**分隔线以下才是正文与操作区(M84 那两条线是占位时期的排版残留)
frame::left_aligned(metrics.column, body.push(action_row(ui, forced, texts)))
@@ -52,6 +56,7 @@ pub(super) fn narrow_detail<'a>(
ui: &'a Ui,
metrics: FrameMetrics,
detail: &'a Detail,
form: &'a LaunchForm,
forced: Option<Interaction>,
texts: Texts,
) -> Element<'a, Message> {
@@ -74,6 +79,7 @@ pub(super) fn narrow_detail<'a>(
.width(Length::Fill);
body = push_text(ui, metrics, body, detail);
body = body.push(super::launch_args::section(ui, &detail.launch, form, texts));
body = body.push(frame::text_role(
ui,
@@ -0,0 +1,218 @@
//! 实例详情页的「启动参数」区(M157)。
//!
//! | 控件 | 语义 | 消息 |
//! |---|---|---|
//! | JVM 参数(多行) | 实例层,与全局 `java.extra_args` **叠加**(实例段最高优先级) | [`Message::Jvm`] |
//! | 游戏参数(多行) | 实例层,追加在命令行最后 | [`Message::Game`] |
//! | 最大内存(单行数字) | 实例层;留空 = 跟随全局默认 | [`Message::Memory`] |
//! | Java 可执行文件(单行) | 实例层;留空 = 跟随全局 → 自动挑 | [`Message::Java`] |
//! | 图形后端(三态 chips) | `default` / `opengl` / `vulkan`;**版本不够时整体灰化 + 提示** | [`Message::Backend`] |
//!
//! 两条判据刻意不在这里重算:
//! 1. **能不能用 `--graphicsBackend`** 来自 [`LaunchFacts::allowed`](core 的 `graphics_backend_allowed`,
//! presenter 的 host 已经算好)——UI 不复制版本号比较;
//! 2. **生效值**(内存/Java/后端)同样来自 facts,表单里画的是实例层原文。
//!
//! 文案全走 [`Texts`](守门见 `instances/tests/i18n.rs` 的白名单与死 key 扫描)。
use iced::widget::{column, container, row};
use iced::{Background, Element, Length};
use unlucky_design::color::Role;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::app::components::buttons::{self, Button};
use crate::app::components::inputs::{text_field, Field};
use crate::app::components::selection::{chip, Chip};
use crate::theme::Ui;
use super::super::model::LaunchFacts;
use super::super::Message as PageMessage;
use super::state::{LaunchForm, Message};
use super::Texts;
/// 页内消息 → 页面消息(本文件的控件树统一画 `pages::Message`)。
fn page(message: Message) -> PageMessage {
PageMessage::Instances(message)
}
/// 「启动参数」区(在详情列的正文段之后、动作行之前)。
pub(super) fn section<'a>(
ui: &'a Ui,
facts: &'a LaunchFacts,
form: &'a LaunchForm,
texts: Texts,
) -> Element<'a, PageMessage> {
let mut body = column![frame_title(ui, texts.launch_title())]
.spacing(ui.space(Space::X3))
.width(Length::Fill);
body = body.push(args_field(
ui,
texts.launch_jvm(),
texts.launch_jvm_hint(),
form.jvm.as_str(),
|value| page(Message::Jvm(value)),
));
body = body.push(args_field(
ui,
texts.launch_game(),
texts.launch_game_hint(),
form.game.as_str(),
|value| page(Message::Game(value)),
));
body = body.push(text_field_with_hint(
ui,
texts.launch_memory(),
form.memory.as_str(),
super::super::frame::text_role_owned(
ui,
TypeRole::BodySmall,
false,
texts.launch_memory_hint(facts.memory_effective_mb),
ui.color(Role::OnSurfaceVariant),
),
|value| page(Message::Memory(value)),
));
body = body.push(text_field_with_hint(
ui,
texts.launch_java(),
form.java.as_str(),
super::super::frame::text_role_owned(
ui,
TypeRole::BodySmall,
false,
texts.launch_java_hint().to_string(),
ui.color(Role::OnSurfaceVariant),
),
|value| page(Message::Java(value)),
));
body = body.push(backend_row(ui, facts, form, texts));
body = body.push(actions_row(ui, texts));
container(body)
.width(Length::Fill)
.padding(ui.space(Space::X3))
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
border: ui.outlined(ShapeScale::Medium),
..container::Style::default()
})
.into()
}
fn frame_title<'a>(ui: &'a Ui, title: &'a str) -> Element<'a, PageMessage> {
super::super::frame::text_role(
ui,
TypeRole::LabelMedium,
true,
title,
ui.color(Role::OnSurfaceVariant),
)
}
/// 一段参数输入 + 一行提示(单行受控输入;参数按空白切分,与 core 的 `tokenize` 同口径)。
fn args_field<'a>(
ui: &'a Ui,
label: &'a str,
hint: &'a str,
value: &'a str,
on_input: impl Fn(String) -> PageMessage + 'a,
) -> Element<'a, PageMessage> {
column![
text_field(ui, Field::new(value).label(label), on_input),
super::super::frame::text_role(
ui,
TypeRole::BodySmall,
false,
hint,
ui.color(Role::OnSurfaceVariant),
),
]
.spacing(ui.space(Space::X1))
.width(Length::Fill)
.into()
}
/// 单行输入 + 一行提示(提示是**拥有的**字符串,所以不能走 `Field::supporting` 的借用口径)。
fn text_field_with_hint<'a>(
ui: &'a Ui,
label: &'a str,
value: &'a str,
hint: Element<'a, PageMessage>,
on_input: impl Fn(String) -> PageMessage + 'a,
) -> Element<'a, PageMessage> {
column![
text_field(ui, Field::new(value).label(label), on_input),
hint,
]
.spacing(ui.space(Space::X1))
.width(Length::Fill)
.into()
}
/// 三态后端:版本不够时整体禁用(chips 不带 `on_press`)+ 一行门槛提示。
fn backend_row<'a>(
ui: &'a Ui,
facts: &'a LaunchFacts,
form: &'a LaunchForm,
texts: Texts,
) -> Element<'a, PageMessage> {
let allowed = facts.allowed;
let mut chips = row![].spacing(ui.space(Space::X2));
for id in &facts.options {
let label = texts.backend_label(id).unwrap_or(id.as_str());
let selected = form.backend == *id;
let spec = if allowed {
Chip::filter(label, selected, page(Message::Backend(id.clone())))
} else {
Chip::filter(label, selected, page(Message::Backend(id.clone()))).disabled()
};
chips = chips.push(chip(ui, spec));
}
let mut meta = column![
super::super::frame::text_role(
ui,
TypeRole::BodySmall,
false,
texts.launch_backend(),
ui.color(Role::OnSurface),
),
chips,
super::super::frame::text_role_owned(
ui,
TypeRole::BodySmall,
false,
texts.launch_effective(&facts.backend_effective),
ui.color(Role::OnSurfaceVariant),
),
]
.spacing(ui.space(Space::X1))
.width(Length::Fill);
if !allowed {
// 灰化 + 明确提示:既不说「已生效」,也不静默吞掉用户的选择。
meta = meta.push(super::super::frame::text_role_owned(
ui,
TypeRole::BodySmall,
false,
texts.launch_backend_gated(&facts.mc_version),
ui.color(Role::Error),
));
}
meta.into()
}
fn actions_row<'a>(ui: &'a Ui, texts: Texts) -> Element<'a, PageMessage> {
row![
buttons::filled(ui, Button::new(texts.launch_save(), page(Message::Save))),
buttons::text(ui, Button::new(texts.launch_reset(), page(Message::Reset))),
]
.spacing(ui.space(Space::X2))
.into()
}
@@ -0,0 +1,190 @@
//! 实例页的**页面状态**(M157):启动参数区的表单。
//!
//! ## 为什么状态在 app、页面只借一帧
//!
//! iced 的 `view` 不能持有跨帧状态,而启动参数区是可编辑的(两个多行框 + 两行单行框 +
//! 三态 chips)。所以表单状态的所有者是 `App`(`app/state.rs` 的 `launch_form` 字段),
//! 页面层只在这一帧借用;这与设置页「组状态归 `App`」是同一条纪律(`pages/settings/state.rs`)。
//!
//! ## 与快照的关系(唯一的重播种点)
//!
//! [`LaunchForm::sync`] 由 `App::refresh_view` 调用:
//!
//! | 情况 | 行为 |
//! |---|---|
//! | 换了一个实例 | **重播种**(用户没写完的草稿不能串到别的实例上) |
//! | 同一个实例、没改过 | 重播种(保存/外部改了配置后跟着刷新) |
//! | 同一个实例、改过(`dirty`) | **不覆盖**(每来一条事件就把输入抹掉是不可接受的) |
//!
//! 「留空 = 跟随下一层」由提交时把空串原样交给 host 表达(`Some("")` = 清空),
//! 表单自己不做任何回落计算——回落只发生在 core。
use unlucky_presenter::dto::{LaunchArgsPatch, LaunchArgsView};
/// 启动参数表单(五项 + 脏标记 + 绑定的实例 id)。
#[derive(Debug)]
pub struct LaunchForm {
/// 这份草稿属于哪个实例(`None` = 还没播种)。
pub bound: Option<String>,
/// 实例级 JVM 参数(空白分隔的文本)。
pub jvm: String,
/// 实例级游戏参数(空白分隔的文本)。
pub game: String,
/// 最大堆(文本形态,空 = 跟随全局)。
pub memory: String,
/// Java 可执行文件(空 = 跟随全局 / 自动挑)。
pub java: String,
/// 图形后端(`default` / `opengl` / `vulkan`)。
pub backend: String,
/// 用户改过、还没保存。
pub dirty: bool,
}
impl Default for LaunchForm {
fn default() -> Self {
Self::from_view(None, &LaunchArgsView::default())
}
}
impl LaunchForm {
/// 用一份快照播种(`bound` 为 `None` = 还没绑定实例)。
#[must_use]
pub fn from_view(bound: Option<&str>, view: &LaunchArgsView) -> Self {
Self {
bound: bound.map(str::to_string),
jvm: view.jvm_args.clone(),
game: view.game_args.clone(),
memory: view.memory_mb.map(|mb| mb.to_string()).unwrap_or_default(),
java: view.java_path.clone(),
backend: view.graphics_backend.clone(),
dirty: false,
}
}
/// 这份草稿是否正对着该实例。
#[must_use]
pub fn bound_to(&self, id: &str) -> bool {
self.bound.as_deref() == Some(id)
}
/// 重播种(见模块文档的三档)。
pub fn sync(&mut self, view: &LaunchArgsView) {
let same_instance = self.bound_to(&view.instance_id);
if same_instance && self.dirty {
return;
}
*self = Self::from_view(Some(&view.instance_id), view);
}
/// 提交补丁:**五项全给**(表单是整份可见的,不用增量语义会让「清空」无法表达)。
#[must_use]
pub fn patch(&self) -> LaunchArgsPatch {
LaunchArgsPatch {
jvm_args: Some(self.jvm.clone()),
game_args: Some(self.game.clone()),
memory_mb: Some(self.memory.clone()),
java_path: Some(self.java.clone()),
graphics_backend: Some(self.backend.clone()),
}
}
/// 「恢复跟随全局」:五项清空,脏标记置位(等用户点保存)。
pub fn reset(&mut self) {
self.jvm.clear();
self.game.clear();
self.memory.clear();
self.java.clear();
self.backend = "default".to_string();
self.dirty = true;
}
}
/// 启动参数区的消息(`pages::Message::Instances` 的载荷)。
///
/// 只表达**页面意图**:编辑写进表单,保存/恢复才需要 app 层理解(前者 dispatch 一条
/// `AppCommand::SetInstanceLaunchArgs`,后者只是清表单)。
#[derive(Debug, Clone)]
pub enum Message {
/// 编辑 JVM 参数。
Jvm(String),
/// 编辑游戏参数。
Game(String),
/// 编辑最大内存(单行文本)。
Memory(String),
/// 编辑 Java 路径(单行文本)。
Java(String),
/// 选图形后端(三态 chips)。
Backend(String),
/// 保存(派发命令)。
Save,
/// 恢复跟随全局(清表单,仍需保存)。
Reset,
}
#[cfg(test)]
mod tests {
use super::*;
fn view(instance: &str, jvm: &str, memory: Option<u32>, backend: &str) -> LaunchArgsView {
LaunchArgsView {
instance_id: instance.to_string(),
jvm_args: jvm.to_string(),
game_args: String::new(),
memory_mb: memory,
memory_effective_mb: memory.unwrap_or(2048),
java_path: String::new(),
java_effective: String::new(),
graphics_backend: backend.to_string(),
graphics_backend_effective: backend.to_string(),
graphics_backend_allowed: true,
mc_version: "26.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(),
],
}
}
/// 换实例必重播种;同实例的脏草稿不被事件冲掉。
#[test]
fn sync_reseeds_per_instance_but_keeps_dirty_edits() {
let mut form = LaunchForm::default();
form.sync(&view("a", "-Xss4m", Some(4096), "vulkan"));
assert_eq!(form.jvm, "-Xss4m");
assert_eq!(form.memory, "4096");
assert!(!form.dirty);
// 用户改动 → 脏 → 同实例的快照刷新不许覆盖。
form.memory = "8192".to_string();
form.dirty = true;
form.sync(&view("a", "-Xss4m", Some(4096), "vulkan"));
assert_eq!(form.memory, "8192", "脏草稿被事件冲掉了");
// 换实例 → 重播种(草稿不串台)。
form.sync(&view("b", "", None, "default"));
assert_eq!(form.memory, "");
assert!(!form.dirty);
assert!(form.bound_to("b"));
}
/// 补丁五项全给:空串就是「清空」,不留 `None` 让 host 猜。
#[test]
fn patch_always_covers_all_five_fields() {
let mut form = LaunchForm::from_view(Some("a"), &view("a", "", None, "default"));
form.memory = "6144".to_string();
form.backend = "opengl".to_string();
let patch = form.patch();
assert_eq!(patch.jvm_args.as_deref(), Some(""));
assert_eq!(patch.memory_mb.as_deref(), Some("6144"));
assert_eq!(patch.graphics_backend.as_deref(), Some("opengl"));
form.reset();
assert!(form.dirty);
let patch = form.patch();
assert_eq!(patch.jvm_args.as_deref(), Some(""));
assert_eq!(patch.memory_mb.as_deref(), Some(""));
assert_eq!(patch.graphics_backend.as_deref(), Some("default"));
}
}
@@ -28,14 +28,18 @@ use super::super::Texts;
///
/// - `Java`:运行时品牌名(`JAVA` 那格的值域里也全是 `Temurin 21` 这类品牌串),翻成「爪哇」是错的;
/// - `pid`:进程号的技术标识。用户拿它去任务管理器里搜进程,翻成「进程」就丢了这条可操作性。
const ALLOWED: &[&str] = &["Java", "pid"];
const ALLOWED: &[&str] = &["Java", "pid", "JVM", "OpenGL", "Vulkan", "MiB"];
/// 页面取词的五个源码文件(死 key 扫描与旧串扫描共用)。
const PAGE_SOURCES: [(&str, &str); 5] = [
/// 页面取词的六个源码文件(死 key 扫描与旧串扫描共用)。
const PAGE_SOURCES: [(&str, &str); 6] = [
("instances.rs", include_str!("../../instances.rs")),
("instances/detail.rs", include_str!("../detail.rs")),
("instances/demo.rs", include_str!("../demo.rs")),
("instances/text.rs", include_str!("../text.rs")),
(
"instances/launch_args.rs",
include_str!("../launch_args.rs"),
),
(
"app/view/instances.rs",
include_str!("../../../app/view/instances.rs"),
@@ -57,13 +61,29 @@ const PAGE_SOURCES: [(&str, &str); 5] = [
/// | `isolated` / `shared` | core 的隔离档位**稳定 id**(`IsolationMode::as_str`),`isolation()` 只认这两档,其余原样上屏 |
/// | `pid` / `reason` | `Catalog::render` 的**占位符名**(与 catalog 里的 `{pid}` / `{reason}` 对应) |
/// | `Instances` | `Texts::keys()` 的**族名前缀**(`Key::ALL` 的过滤条件,不是文案) |
const ALLOWED_LITERALS: [&str; 6] = [
const ALLOWED_LITERALS: [&str; 17] = [
"{date} {time} UTC",
"isolated",
"shared",
"pid",
"reason",
"Instances",
// M157:图形后端三态的**稳定 id**(与 core 的 `GraphicsBackend::IDS` 同一份词表),
// `backend_label` 只认这三档,其余原样上屏。
"default",
"opengl",
"vulkan",
// M157:`Catalog::render` 的占位符名(`{version}` / `{value}` / `{default}`),不是文案。
"version",
"value",
// M157:`instances/demo.rs` 的**出图夹具**(用户数据的形状,不是界面文案):
// 一个示例 JVM 参数、两个示例 MC 版本、一个示例 Java 路径,以及夹具开关的环境变量名与取值。
"-XX:+UseG1GC",
"26.2-snapshot-2",
"1.20.4",
"C:/Java/temurin-21/bin/java.exe",
"ULL_LAUNCH_ARGS_FIXTURE",
"gated",
];
/// 抽出源码里的字符串字面量(够用的实现:本仓这五个文件没有 `\"` 转义、没有多行字符串)。
@@ -248,9 +268,13 @@ fn every_english_literal_in_the_page_sources_is_whitelisted() {
"text.rs 没有引用本族的 key"
);
assert!(
PAGE_SOURCES[4].1.contains("Key::InstancesStage"),
PAGE_SOURCES[5].1.contains("Key::InstancesStage"),
"阶段标签没有走目录"
);
assert!(
PAGE_SOURCES[4].1.contains("texts.launch_title()"),
"启动参数区没有走目录"
);
}
/// **死 key 守门**:目录里每一条 `Instances*` 都必须被页面(或产品快照转换)真的引用。
@@ -85,6 +85,67 @@ impl Texts {
self.text(Key::InstancesNarrowNote)
}
// ------------------------------------------------------------- 启动参数(M157)
pub(super) fn launch_title(self) -> &'static str {
self.text(Key::InstancesLaunchTitle)
}
pub(super) fn launch_jvm(self) -> &'static str {
self.text(Key::InstancesLaunchJvm)
}
pub(super) fn launch_jvm_hint(self) -> &'static str {
self.text(Key::InstancesLaunchJvmHint)
}
pub(super) fn launch_game(self) -> &'static str {
self.text(Key::InstancesLaunchGame)
}
pub(super) fn launch_game_hint(self) -> &'static str {
self.text(Key::InstancesLaunchGameHint)
}
pub(super) fn launch_memory(self) -> &'static str {
self.text(Key::InstancesLaunchMemory)
}
/// 「留空 = 跟随全局默认({default} MiB)」,`default` 是生效值(三级回落之后)。
pub(super) fn launch_memory_hint(self, default_mb: u32) -> String {
self.0.render(
Key::InstancesLaunchMemoryHint,
&[("default", &default_mb.to_string())],
)
}
pub(super) fn launch_java(self) -> &'static str {
self.text(Key::InstancesLaunchJava)
}
pub(super) fn launch_java_hint(self) -> &'static str {
self.text(Key::InstancesLaunchJavaHint)
}
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)])
}
/// 「生效:vulkan」。
pub(super) fn launch_effective(self, value: &str) -> String {
self.0
.render(Key::InstancesLaunchEffective, &[("value", value)])
}
/// 三态后端的显示名(稳定 id → 文案);**未知 id 返回 `None`**(调用方原样上屏,不猜)。
pub(super) fn backend_label(self, id: &str) -> Option<&'static str> {
Some(self.text(match id {
"default" => Key::InstancesBackendDefault,
"opengl" => Key::InstancesBackendOpengl,
"vulkan" => Key::InstancesBackendVulkan,
_ => return None,
}))
}
pub(super) fn launch_save(self) -> &'static str {
self.text(Key::InstancesLaunchSave)
}
pub(super) fn launch_reset(self) -> &'static str {
self.text(Key::InstancesLaunchReset)
}
fn text(self, key: Key) -> &'static str {
self.0.text(key)
}