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:
1 parent
d68523b02a
commit
e2021645d7
56 files changed
+3776
-607
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user