fix(launch): M157 r3 周快照门限按继承根版本 + 保存失败保留草稿(复审 P2-1/P2-2)
P2-1 周快照 + 加载器的 --graphicsBackend 静默丢失:presenter host::launch::build_request 与 cli snapshot::prepare 给 SnapshotInput.game_version 传 None,装配按带加载器后缀的 version_id(26w14a-fabric-0.16.5)判门限,GameVersion::is_weekly_snapshot 的 6 字符形态认不出来。两个调用点改为传继承链的 MC 根版本(25 行注释说明「门槛与拼装必须同一口径」)。回归:core launch::overrides 的周快照实际装配用例(正/负两档)、presenter host::launch::tests、cli snapshot::tests 各一条根版本断言。 P2-2 保存失败丢草稿:app/update/instances.rs 的 Save 分支派发前就把 launch_form.dirty 清掉,失败事件后 pump 用旧快照重建视图并覆盖表单。改为派发不清脏;LaunchForm::sync 只在「回程快照已等于表单」(= 落盘确认)时清脏并重播种,否则保留草稿。回归:failed_save_keeps_the_draft / confirmed_save_clears_dirty_and_reseeds / save_dispatch_keeps_the_dirty_flag。 顺带:launch::overrides 测试拆到 overrides/tests.rs(守 400 行)。
This commit is contained in:
1 parent
c76a1437e6
commit
211e07b470
9 files changed
+640
-311
No files matched your search
@@ -53,8 +53,11 @@ impl App {
|
|||||||
self.launch_form.reset();
|
self.launch_form.reset();
|
||||||
}
|
}
|
||||||
InstanceMessage::Save => {
|
InstanceMessage::Save => {
|
||||||
|
// **不在这里清脏**:`dispatch` 只是把命令送出去,落盘成败要等回程事件。
|
||||||
|
// 失败时(内存 `4g` / 写盘失败)pump 会带**旧快照**重建视图,脏标记若已清掉,
|
||||||
|
// `LaunchForm::sync` 就会用旧值覆盖用户刚敲的草稿(M157 r3 复审 P2-2)。
|
||||||
|
// 清脏的唯一时机是「回程快照已经等于表单」——见 `LaunchForm::sync`。
|
||||||
let patch: LaunchArgsPatch = self.launch_form.patch();
|
let patch: LaunchArgsPatch = self.launch_form.patch();
|
||||||
self.launch_form.dirty = false;
|
|
||||||
self.dispatch(AppCommand::set_launch_args(&instance, patch));
|
self.dispatch(AppCommand::set_launch_args(&instance, patch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,13 +152,22 @@ mod tests {
|
|||||||
assert!(!app.launch_form.dirty);
|
assert!(!app.launch_form.dirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保存之后脏标记清掉(否则下一次快照刷新不敢重播种,界面会一直显示旧值)。
|
/// **保存只派发、不清脏**(M157 r3 复审 P2-2):落盘成败要等回程事件,
|
||||||
|
/// 派发前清掉脏标记会让失败后的重播覆盖用户草稿。
|
||||||
|
///
|
||||||
|
/// 抓得住:Save 分支里又写回 `dirty = false`(那条路径在失败时丢草稿)。
|
||||||
|
/// 抓不住:回程事件的真实时序——清脏判据在 `LaunchForm::sync` 的
|
||||||
|
/// `confirmed_save_clears_dirty_and_reseeds` / `failed_save_keeps_the_draft` 里。
|
||||||
#[test]
|
#[test]
|
||||||
fn save_clears_the_dirty_flag() {
|
fn save_dispatch_keeps_the_dirty_flag() {
|
||||||
let mut app = app_with_form();
|
let mut app = app_with_form();
|
||||||
let _ = app.on_instance_message(InstanceMessage::Java("D:/jdk21/bin/java.exe".to_string()));
|
let _ = app.on_instance_message(InstanceMessage::Memory("4g".to_string()));
|
||||||
assert!(app.launch_form.dirty);
|
assert!(app.launch_form.dirty);
|
||||||
let _ = app.on_instance_message(InstanceMessage::Save);
|
let _ = app.on_instance_message(InstanceMessage::Save);
|
||||||
assert!(!app.launch_form.dirty);
|
assert!(
|
||||||
|
app.launch_form.dirty,
|
||||||
|
"派发后仍要保留脏标记,等回程快照确认"
|
||||||
|
);
|
||||||
|
assert_eq!(app.launch_form.memory, "4g", "派发不改表单内容");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,14 +68,36 @@ impl LaunchForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 重播种(见模块文档的三档)。
|
/// 重播种(见模块文档的三档)。
|
||||||
|
///
|
||||||
|
/// **脏草稿只在「落盘已确认」时被替换**:保存把命令派发出去之后,回程可能是一条错误事件
|
||||||
|
/// (内存写了 `4g`、实例文件写不进去),那时 pump 会带着**旧快照**重建视图——如果这里无条件
|
||||||
|
/// 覆盖,用户刚敲的东西就没了(M157 r3 复审 P2-2)。判据是「快照已经等于表单」:
|
||||||
|
/// 相等 ⇒ 这次保存落了盘,清脏并重播种;不相等 ⇒ 保留草稿等用户改。
|
||||||
pub fn sync(&mut self, view: &LaunchArgsView) {
|
pub fn sync(&mut self, view: &LaunchArgsView) {
|
||||||
let same_instance = self.bound_to(&view.instance_id);
|
let same_instance = self.bound_to(&view.instance_id);
|
||||||
if same_instance && self.dirty {
|
if same_instance && self.dirty {
|
||||||
|
if self.matches(view) {
|
||||||
|
self.dirty = false;
|
||||||
|
*self = Self::from_view(Some(&view.instance_id), view);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*self = Self::from_view(Some(&view.instance_id), view);
|
*self = Self::from_view(Some(&view.instance_id), view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 表单当前内容是否与一份快照(= 盘上已持久化的值)完全一致。
|
||||||
|
///
|
||||||
|
/// 比较的是**文本口径**:内存按「快照有值 → 十进制串,无值 → 空串」对齐,其余直接比原文。
|
||||||
|
#[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.java.trim() == view.java_path.trim()
|
||||||
|
&& self.backend == view.graphics_backend
|
||||||
|
}
|
||||||
|
|
||||||
/// 提交补丁:**五项全给**(表单是整份可见的,不用增量语义会让「清空」无法表达)。
|
/// 提交补丁:**五项全给**(表单是整份可见的,不用增量语义会让「清空」无法表达)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn patch(&self) -> LaunchArgsPatch {
|
pub fn patch(&self) -> LaunchArgsPatch {
|
||||||
@@ -169,6 +191,44 @@ mod tests {
|
|||||||
assert!(form.bound_to("b"));
|
assert!(form.bound_to("b"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **保存失败不许吃掉草稿**(M157 r3 复审 P2-2):脏表单 + 旧快照回来 ⇒ 原样保留。
|
||||||
|
///
|
||||||
|
/// 抓得住:`sync` 在「同实例 + 脏」时无条件重播种(用户敲的 `4g` 被磁盘上的 4096 覆盖)。
|
||||||
|
/// 抓不住:真实事件时序(那在 `app/update/instances.rs` 的用例里按消息顺序模拟)。
|
||||||
|
#[test]
|
||||||
|
fn failed_save_keeps_the_draft() {
|
||||||
|
let mut form = LaunchForm::from_view(Some("a"), &view("a", "", Some(4096), "default"));
|
||||||
|
// 用户把内存改成非法值(host 会拒),此时表单是脏的。
|
||||||
|
form.memory = "4g".to_string();
|
||||||
|
form.jvm = "-Xmx3g".to_string();
|
||||||
|
form.dirty = true;
|
||||||
|
|
||||||
|
// 回程事件带来的仍是**旧**快照(保存失败 ⇒ 盘上没变)。
|
||||||
|
form.sync(&view("a", "", Some(4096), "default"));
|
||||||
|
|
||||||
|
assert_eq!(form.memory, "4g", "失败后的重播把用户正在改的内存吃掉了");
|
||||||
|
assert_eq!(form.jvm, "-Xmx3g");
|
||||||
|
assert!(form.dirty, "草稿还在,脏标记也不能掉");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **保存成功要清脏**:回程快照等于表单 ⇒ 这一步算落盘确认,之后可以跟着外部变化重播种。
|
||||||
|
#[test]
|
||||||
|
fn confirmed_save_clears_dirty_and_reseeds() {
|
||||||
|
let mut form = LaunchForm::from_view(Some("a"), &view("a", "", Some(4096), "default"));
|
||||||
|
form.memory = "8192".to_string();
|
||||||
|
form.backend = "vulkan".to_string();
|
||||||
|
form.dirty = true;
|
||||||
|
|
||||||
|
form.sync(&view("a", "", Some(8192), "vulkan"));
|
||||||
|
assert!(!form.dirty, "盘上已经是这份值了,脏标记该清");
|
||||||
|
assert_eq!(form.memory, "8192");
|
||||||
|
|
||||||
|
// 之后外部改了配置(比如 CLI 写入)⇒ 干净的表格跟着更新。
|
||||||
|
form.sync(&view("a", "-Xss4m", Some(2048), "opengl"));
|
||||||
|
assert_eq!(form.memory, "2048");
|
||||||
|
assert_eq!(form.jvm, "-Xss4m");
|
||||||
|
}
|
||||||
|
|
||||||
/// 补丁五项全给:空串就是「清空」,不留 `None` 让 host 猜。
|
/// 补丁五项全给:空串就是「清空」,不留 `None` 让 host 猜。
|
||||||
#[test]
|
#[test]
|
||||||
fn patch_always_covers_all_five_fields() {
|
fn patch_always_covers_all_five_fields() {
|
||||||
|
|||||||
@@ -241,7 +241,14 @@ fn backend_gate(ctx: &Ctx, version_id: &str) -> (bool, String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 继承链根(原版层)的 id——能力位与版本门限都以它为准(loader 层的 id 带加载器后缀)。
|
/// 继承链根(原版层)的 id——能力位与版本门限都以它为准(loader 层的 id 带加载器后缀)。
|
||||||
fn root_version_id(layout: &unlucky_core::game::GameLayout, manifest: &VersionJson) -> String {
|
///
|
||||||
|
/// `pub(crate)`:`snapshot::prepare` 的装配(`SnapshotInput.game_version`)也要这一份口径,
|
||||||
|
/// 门槛判定(本模块的 `backend_gate`)与拼装必须用**同一个**根版本,否则会出现
|
||||||
|
/// 「UI 说可用、命令行里没有这个参数」的静默偏差。
|
||||||
|
pub(crate) fn root_version_id(
|
||||||
|
layout: &unlucky_core::game::GameLayout,
|
||||||
|
manifest: &VersionJson,
|
||||||
|
) -> String {
|
||||||
let mut current = manifest.id.clone().unwrap_or_default();
|
let mut current = manifest.id.clone().unwrap_or_default();
|
||||||
let mut parent = manifest.inherits_from.clone().filter(|id| !id.is_empty());
|
let mut parent = manifest.inherits_from.clone().filter(|id| !id.is_empty());
|
||||||
for _ in 0..16 {
|
for _ in 0..16 {
|
||||||
|
|||||||
@@ -131,11 +131,14 @@ pub fn prepare(ctx: &Ctx, input: &PlanInput) -> Result<Plan, CliError> {
|
|||||||
.clone()
|
.clone()
|
||||||
.or_else(|| resolved.asset_index.as_ref().map(|index| index.id.clone()));
|
.or_else(|| resolved.asset_index.as_ref().map(|index| index.id.clone()));
|
||||||
|
|
||||||
|
// 同 presenter:装配门限按**继承链的 MC 根版本**判(loader 实例的 version_id 带加载器后缀,
|
||||||
|
// 周快照的 6 字符形态判据在那儿失效,`--graphicsBackend` 会被静默跳过)。
|
||||||
|
let root_version = super::instance_launch::root_version_id(&game, &manifest);
|
||||||
let snapshot = SnapshotInput {
|
let snapshot = SnapshotInput {
|
||||||
version_id: &version_id,
|
version_id: &version_id,
|
||||||
version_name: Some(&input.instance_name),
|
version_name: Some(&input.instance_name),
|
||||||
version_type: None,
|
version_type: None,
|
||||||
game_version: None,
|
game_version: Some(root_version.as_str()),
|
||||||
main_class: resolved.main_class.as_deref(),
|
main_class: resolved.main_class.as_deref(),
|
||||||
jvm_arguments: &expanded.jvm,
|
jvm_arguments: &expanded.jvm,
|
||||||
game_arguments: &expanded.game,
|
game_arguments: &expanded.game,
|
||||||
@@ -235,3 +238,94 @@ fn describe_warning(warning: &AdapterWarning) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// 一次性临时数据根(`--data-dir` 的等价物:custom 模式 → `<root>/.ULL` + `<root>/.minecraft`)。
|
||||||
|
struct TempDir(PathBuf);
|
||||||
|
|
||||||
|
impl TempDir {
|
||||||
|
fn new(tag: &str) -> Self {
|
||||||
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"ull-cli-snapshot-{tag}-{}-{seq}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_dir_all(&path);
|
||||||
|
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ctx_at(dir: &TempDir) -> Ctx {
|
||||||
|
let globals = crate::cli::Globals {
|
||||||
|
data_dir: Some(dir.path().to_path_buf()),
|
||||||
|
..crate::cli::Globals::default()
|
||||||
|
};
|
||||||
|
Ctx::resolve(&globals).expect("解析数据根")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_version(ctx: &Ctx, id: &str, json: &str) {
|
||||||
|
let path = ctx.game().version_json(id);
|
||||||
|
std::fs::create_dir_all(path.parent().expect("版本目录")).expect("版本目录");
|
||||||
|
std::fs::write(&path, json).expect("写版本 JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **周快照 + 加载器实例**(M157 r3 复审 P2-1):`prepare` 交给装配的游戏版本是
|
||||||
|
/// 继承链根的 `26w14a`,不是 `26w14a-fabric-0.16.5`。
|
||||||
|
///
|
||||||
|
/// 抓得住:`SnapshotInput.game_version` 回退成 `None`(装配用带后缀的 id 判门限,
|
||||||
|
/// `--graphicsBackend` 会被静默跳过,而 CLI 自己的门槛显示「可下发」)。
|
||||||
|
/// 抓不住:完整 argv(`crates/core/src/launch/overrides/tests.rs` 的周快照用例)。
|
||||||
|
#[test]
|
||||||
|
fn weekly_loader_instance_is_assembled_against_the_mc_root_version() {
|
||||||
|
let dir = TempDir::new("weekly");
|
||||||
|
let ctx = ctx_at(&dir);
|
||||||
|
write_version(
|
||||||
|
&ctx,
|
||||||
|
"26w14a",
|
||||||
|
r#"{"id":"26w14a","mainClass":"net.minecraft.client.main.Main",
|
||||||
|
"libraries":[{"name":"org.lwjgl:lwjgl-vulkan:3.4.3"}]}"#,
|
||||||
|
);
|
||||||
|
write_version(
|
||||||
|
&ctx,
|
||||||
|
"26w14a-fabric-0.16.5",
|
||||||
|
r#"{"id":"26w14a-fabric-0.16.5","inheritsFrom":"26w14a",
|
||||||
|
"mainClass":"net.fabricmc.loader.impl.launch.knot.KnotClient"}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let plan = prepare(
|
||||||
|
&ctx,
|
||||||
|
&PlanInput {
|
||||||
|
version_id: "26w14a-fabric-0.16.5".to_string(),
|
||||||
|
instance_name: "周快照 Fabric".to_string(),
|
||||||
|
..PlanInput::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("装配应成功");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
plan.request.instance.version.raw(),
|
||||||
|
"26w14a",
|
||||||
|
"装配的游戏版本必须是继承链的 MC 根版本(CLI 门槛与拼装必须同一口径)"
|
||||||
|
);
|
||||||
|
assert!(plan.request.instance.version.is_weekly_snapshot());
|
||||||
|
assert!(plan.request.manifest.has_vulkan_backend);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,302 +71,4 @@ fn tokens(text: Option<&str>) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::game::launch_overrides::{
|
|
||||||
resolve, GlobalLaunchDefaults, GraphicsBackend, LaunchOverrides,
|
|
||||||
};
|
|
||||||
use crate::launch::args_table::{assemble, AssemblyWarning};
|
|
||||||
use crate::launch::game_version::GameVersion;
|
|
||||||
use crate::launch::platform::{OsFamily, PlatformCtx};
|
|
||||||
use crate::launch::request::{
|
|
||||||
AuthPlaceholders, InstanceInfo, JavaSelection, LaunchDirs, LauncherInfo, Log4jOptions,
|
|
||||||
ManifestArgs, OptimizationOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn request(version: &str) -> LaunchRequest {
|
|
||||||
LaunchRequest {
|
|
||||||
instance: InstanceInfo {
|
|
||||||
id: "inst-1".to_string(),
|
|
||||||
name: version.to_string(),
|
|
||||||
version: GameVersion::parse(version),
|
|
||||||
version_type: "release".to_string(),
|
|
||||||
},
|
|
||||||
launcher: LauncherInfo {
|
|
||||||
name: "UnluckyLauncher".to_string(),
|
|
||||||
version: "0.2.2".to_string(),
|
|
||||||
},
|
|
||||||
java: JavaSelection::new("C:/java/bin/java.exe", 21, 64),
|
|
||||||
memory: MemoryOptions {
|
|
||||||
min_mb: None,
|
|
||||||
max_mb: None,
|
|
||||||
},
|
|
||||||
manifest: ManifestArgs {
|
|
||||||
main_class: Some("net.minecraft.client.main.Main".to_string()),
|
|
||||||
..ManifestArgs::default()
|
|
||||||
},
|
|
||||||
dirs: LaunchDirs {
|
|
||||||
game_dir: PathBuf::from("C:/mc"),
|
|
||||||
primary_jar: PathBuf::from("C:/mc/versions/x/x.jar"),
|
|
||||||
..LaunchDirs::default()
|
|
||||||
},
|
|
||||||
classpath: vec![PathBuf::from("C:/mc/libraries/a.jar")],
|
|
||||||
auth: AuthPlaceholders::default(),
|
|
||||||
log4j: Log4jOptions {
|
|
||||||
enabled: false,
|
|
||||||
..Log4jOptions::default()
|
|
||||||
},
|
|
||||||
optimization: OptimizationOptions {
|
|
||||||
enabled: false,
|
|
||||||
physical_memory_mb: None,
|
|
||||||
},
|
|
||||||
language: "zh-CN".to_string(),
|
|
||||||
..LaunchRequest::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ctx() -> PlatformCtx {
|
|
||||||
PlatformCtx::new(OsFamily::Windows, "x86_64")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn argvs(req: &LaunchRequest) -> Vec<String> {
|
|
||||||
assemble(req, &ctx()).expect("组装应成功").args
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position(args: &[String], needle: &str) -> Option<usize> {
|
|
||||||
args.iter().position(|arg| arg == needle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **叠加规则**:实例段在 `-Xmx` 之前、全局段在之后;两段都下发(不替换)。
|
|
||||||
#[test]
|
|
||||||
fn instance_and_global_jvm_args_stack_in_order() {
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
jvm_args: Some("-XX:+UseG1GC".to_string()),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
let global = GlobalLaunchDefaults {
|
|
||||||
jvm_args: Some("-Xss4m".to_string()),
|
|
||||||
..GlobalLaunchDefaults::default()
|
|
||||||
};
|
|
||||||
let resolved = resolve(Some(&instance), &global);
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
let args = argvs(&req);
|
|
||||||
|
|
||||||
let instance_at = position(&args, "-XX:+UseG1GC").expect("实例参数要下发");
|
|
||||||
let memory_at = position(&args, "-Xmx2048m").expect("内存字段要产 -Xmx");
|
|
||||||
let global_at = position(&args, "-Xss4m").expect("全局参数要下发");
|
|
||||||
assert!(
|
|
||||||
instance_at < memory_at,
|
|
||||||
"实例段(override_jvm)必须在 -Xmx 之前:{args:?}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
global_at > memory_at,
|
|
||||||
"全局段(user.jvm)在 -Xmx 之后(基线不抢内存字段):{args:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **-Xmx 冲突**:实例文本写 `-Xmx` ⇒ 内存字段不再产出(文本赢);全局文本写 `-Xmx` ⇒ 被内存字段抑制。
|
|
||||||
#[test]
|
|
||||||
fn xmx_conflict_follows_the_assembly_order() {
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
jvm_args: Some("-Xmx3g".to_string()),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
let args = argvs(&req);
|
|
||||||
assert!(
|
|
||||||
args.iter().any(|arg| arg == "-Xmx3g"),
|
|
||||||
"实例文本的 -Xmx 必须生效:{args:?}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!args.iter().any(|arg| arg == "-Xmx2048m"),
|
|
||||||
"内存字段的 -Xmx 必须被抑制:{args:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let resolved = resolve(None, &GlobalLaunchDefaults::with_jvm_args("-Xmx3g"));
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
let args = argvs(&req);
|
|
||||||
assert!(
|
|
||||||
args.iter().any(|arg| arg == "-Xmx2048m"),
|
|
||||||
"全局文本的 -Xmx 不生效(第 7 项晚于第 5 项):{args:?}"
|
|
||||||
);
|
|
||||||
assert!(!args.iter().any(|arg| arg == "-Xmx3g"), "{args:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 内存三级回落落进 `-Xmx`:实例 → 全局 → 默认。
|
|
||||||
#[test]
|
|
||||||
fn memory_falls_back_into_xmx() {
|
|
||||||
let global = GlobalLaunchDefaults {
|
|
||||||
memory_mb: Some(6144),
|
|
||||||
..GlobalLaunchDefaults::default()
|
|
||||||
};
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
memory_mb: Some(8192),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
for (overrides, expected) in [(Some(instance), "-Xmx8192m"), (None, "-Xmx6144m")] {
|
|
||||||
let resolved = resolve(overrides.as_ref(), &global);
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
assert!(
|
|
||||||
argvs(&req).iter().any(|arg| arg == expected),
|
|
||||||
"期望 {expected}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let resolved = resolve(None, &GlobalLaunchDefaults::default());
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
assert!(argvs(&req).iter().any(|arg| arg == "-Xmx2048m"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 游戏参数最后追加(可覆盖前面一切游戏参数)。
|
|
||||||
#[test]
|
|
||||||
fn game_args_land_at_the_tail() {
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
game_args: Some("--fullscreen --width 1920".to_string()),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
let args = argvs(&req);
|
|
||||||
let tail = &args[args.len() - 3..];
|
|
||||||
assert_eq!(tail, ["--fullscreen", "--width", "1920"], "{args:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **版本门槛**:26.2-snapshot-2+ 才下发;老版本不下发但必须给诊断告警(不静默)。
|
|
||||||
#[test]
|
|
||||||
fn graphics_backend_is_version_gated_with_a_warning() {
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
graphics_backend: Some(GraphicsBackend::Vulkan),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
|
||||||
|
|
||||||
let mut supported = request("26.2-snapshot-2");
|
|
||||||
apply(&mut supported, &resolved);
|
|
||||||
let command = assemble(&supported, &ctx()).expect("组装");
|
|
||||||
let at = position(&command.args, "--graphicsBackend").expect("支持版本要下发");
|
|
||||||
assert_eq!(command.args[at + 1], "vulkan");
|
|
||||||
assert!(
|
|
||||||
!command
|
|
||||||
.warnings
|
|
||||||
.iter()
|
|
||||||
.any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })),
|
|
||||||
"支持版本不该有跳过告警:{:?}",
|
|
||||||
command.warnings
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut old = request("1.20.4");
|
|
||||||
apply(&mut old, &resolved);
|
|
||||||
let command = assemble(&old, &ctx()).expect("组装");
|
|
||||||
assert!(
|
|
||||||
!command.args.iter().any(|arg| arg == "--graphicsBackend"),
|
|
||||||
"老版本不许下发:{:?}",
|
|
||||||
command.args
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
command
|
|
||||||
.warnings
|
|
||||||
.iter()
|
|
||||||
.any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })),
|
|
||||||
"跳过必须留诊断:{:?}",
|
|
||||||
command.warnings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `default` = 不下发、不告警(三态里的「跟随游戏默认」)。
|
|
||||||
#[test]
|
|
||||||
fn default_backend_emits_nothing() {
|
|
||||||
for instance in [None, Some(LaunchOverrides::default())] {
|
|
||||||
let resolved = resolve(instance.as_ref(), &GlobalLaunchDefaults::default());
|
|
||||||
let mut req = request("26.2");
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
let command = assemble(&req, &ctx()).expect("组装");
|
|
||||||
assert!(!command.args.iter().any(|arg| arg == "--graphicsBackend"));
|
|
||||||
assert!(command.warnings.is_empty(), "{:?}", command.warnings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 启动器自带的 `--fullscreen`(第 29 项)与实例游戏参数共存:实例在最后,可覆盖。
|
|
||||||
#[test]
|
|
||||||
fn instance_game_args_win_over_launcher_defaults() {
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
game_args: Some("--fullscreen".to_string()),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
req.fullscreen = true;
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
let args = argvs(&req);
|
|
||||||
assert_eq!(
|
|
||||||
args.iter()
|
|
||||||
.filter(|arg| arg.as_str() == "--fullscreen")
|
|
||||||
.count(),
|
|
||||||
2,
|
|
||||||
"两处都给 --fullscreen,由 MC 取后者(实例段在最后):{args:?}"
|
|
||||||
);
|
|
||||||
assert_eq!(args.last().map(String::as_str), Some("--fullscreen"));
|
|
||||||
assert!(
|
|
||||||
position(&args, "net.minecraft.client.main.Main").unwrap() < args.len() - 1,
|
|
||||||
"实例游戏参数必须在主类之后"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Java 路径不进请求、只作为调用方的选择依据。
|
|
||||||
#[test]
|
|
||||||
fn java_path_is_a_scalar_for_the_caller() {
|
|
||||||
let instance = LaunchOverrides {
|
|
||||||
java_path: Some(PathBuf::from("D:/jdk21/bin/java.exe")),
|
|
||||||
..LaunchOverrides::default()
|
|
||||||
};
|
|
||||||
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
|
||||||
assert_eq!(
|
|
||||||
java_binary(&resolved),
|
|
||||||
Some(Path::new("D:/jdk21/bin/java.exe"))
|
|
||||||
);
|
|
||||||
let mut req = request("1.20.4");
|
|
||||||
let before = req.java.binary.clone();
|
|
||||||
apply(&mut req, &resolved);
|
|
||||||
assert_eq!(req.java.binary, before, "apply 不改 Java 选择");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `backend_allowed`:继承解析后的库表 + 版本门限,两条判据的**唯一组合入口**。
|
|
||||||
///
|
|
||||||
/// 它是 UI 灰化与拼装链共用的那一格:调用方只负责把「继承解析后的版本 JSON」和
|
|
||||||
/// 「游戏版本号」递进来,不许自己拼 `has_vulkan_backend && is_at_least(..)`。
|
|
||||||
#[test]
|
|
||||||
fn backend_allowed_combines_capability_and_threshold() {
|
|
||||||
use crate::game::VersionJson;
|
|
||||||
|
|
||||||
let with_vulkan = VersionJson::from_json_str(
|
|
||||||
r#"{"id":"x","libraries":[{"name":"org.lwjgl:lwjgl-vulkan:3.4.3"}]}"#,
|
|
||||||
)
|
|
||||||
.expect("解析");
|
|
||||||
let without = VersionJson::from_json_str(r#"{"id":"x"}"#).expect("解析");
|
|
||||||
|
|
||||||
// 可解析版本:精确门限说了算(库表只作周快照兜底)。
|
|
||||||
assert!(backend_allowed(
|
|
||||||
&GameVersion::parse("26.2-snapshot-2"),
|
|
||||||
&without
|
|
||||||
));
|
|
||||||
assert!(!backend_allowed(
|
|
||||||
&GameVersion::parse("26.2-snapshot-1"),
|
|
||||||
&with_vulkan
|
|
||||||
));
|
|
||||||
assert!(!backend_allowed(
|
|
||||||
&GameVersion::parse("1.20.4"),
|
|
||||||
&with_vulkan
|
|
||||||
));
|
|
||||||
// 周快照:看库表。
|
|
||||||
assert!(backend_allowed(&GameVersion::parse("26w14a"), &with_vulkan));
|
|
||||||
assert!(!backend_allowed(&GameVersion::parse("26w14a"), &without));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::game::launch_overrides::{
|
||||||
|
resolve, GlobalLaunchDefaults, GraphicsBackend, LaunchOverrides,
|
||||||
|
};
|
||||||
|
use crate::launch::adapter::{build_request, AdapterOptions, LibraryInput, SnapshotInput};
|
||||||
|
use crate::launch::args_table::{assemble, AssemblyWarning};
|
||||||
|
use crate::launch::game_version::GameVersion;
|
||||||
|
use crate::launch::platform::{OsFamily, PlatformCtx};
|
||||||
|
use crate::launch::request::{
|
||||||
|
AuthPlaceholders, InstanceInfo, JavaSelection, LaunchDirs, LauncherInfo, Log4jOptions,
|
||||||
|
ManifestArgs, OptimizationOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn request(version: &str) -> LaunchRequest {
|
||||||
|
LaunchRequest {
|
||||||
|
instance: InstanceInfo {
|
||||||
|
id: "inst-1".to_string(),
|
||||||
|
name: version.to_string(),
|
||||||
|
version: GameVersion::parse(version),
|
||||||
|
version_type: "release".to_string(),
|
||||||
|
},
|
||||||
|
launcher: LauncherInfo {
|
||||||
|
name: "UnluckyLauncher".to_string(),
|
||||||
|
version: "0.2.2".to_string(),
|
||||||
|
},
|
||||||
|
java: JavaSelection::new("C:/java/bin/java.exe", 21, 64),
|
||||||
|
memory: MemoryOptions {
|
||||||
|
min_mb: None,
|
||||||
|
max_mb: None,
|
||||||
|
},
|
||||||
|
manifest: ManifestArgs {
|
||||||
|
main_class: Some("net.minecraft.client.main.Main".to_string()),
|
||||||
|
..ManifestArgs::default()
|
||||||
|
},
|
||||||
|
dirs: LaunchDirs {
|
||||||
|
game_dir: PathBuf::from("C:/mc"),
|
||||||
|
primary_jar: PathBuf::from("C:/mc/versions/x/x.jar"),
|
||||||
|
..LaunchDirs::default()
|
||||||
|
},
|
||||||
|
classpath: vec![PathBuf::from("C:/mc/libraries/a.jar")],
|
||||||
|
auth: AuthPlaceholders::default(),
|
||||||
|
log4j: Log4jOptions {
|
||||||
|
enabled: false,
|
||||||
|
..Log4jOptions::default()
|
||||||
|
},
|
||||||
|
optimization: OptimizationOptions {
|
||||||
|
enabled: false,
|
||||||
|
physical_memory_mb: None,
|
||||||
|
},
|
||||||
|
language: "zh-CN".to_string(),
|
||||||
|
..LaunchRequest::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ctx() -> PlatformCtx {
|
||||||
|
PlatformCtx::new(OsFamily::Windows, "x86_64")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn argvs(req: &LaunchRequest) -> Vec<String> {
|
||||||
|
assemble(req, &ctx()).expect("组装应成功").args
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(args: &[String], needle: &str) -> Option<usize> {
|
||||||
|
args.iter().position(|arg| arg == needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **叠加规则**:实例段在 `-Xmx` 之前、全局段在之后;两段都下发(不替换)。
|
||||||
|
#[test]
|
||||||
|
fn instance_and_global_jvm_args_stack_in_order() {
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
jvm_args: Some("-XX:+UseG1GC".to_string()),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
let global = GlobalLaunchDefaults {
|
||||||
|
jvm_args: Some("-Xss4m".to_string()),
|
||||||
|
..GlobalLaunchDefaults::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(Some(&instance), &global);
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
let args = argvs(&req);
|
||||||
|
|
||||||
|
let instance_at = position(&args, "-XX:+UseG1GC").expect("实例参数要下发");
|
||||||
|
let memory_at = position(&args, "-Xmx2048m").expect("内存字段要产 -Xmx");
|
||||||
|
let global_at = position(&args, "-Xss4m").expect("全局参数要下发");
|
||||||
|
assert!(
|
||||||
|
instance_at < memory_at,
|
||||||
|
"实例段(override_jvm)必须在 -Xmx 之前:{args:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
global_at > memory_at,
|
||||||
|
"全局段(user.jvm)在 -Xmx 之后(基线不抢内存字段):{args:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **-Xmx 冲突**:实例文本写 `-Xmx` ⇒ 内存字段不再产出(文本赢);全局文本写 `-Xmx` ⇒ 被内存字段抑制。
|
||||||
|
#[test]
|
||||||
|
fn xmx_conflict_follows_the_assembly_order() {
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
jvm_args: Some("-Xmx3g".to_string()),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
let args = argvs(&req);
|
||||||
|
assert!(
|
||||||
|
args.iter().any(|arg| arg == "-Xmx3g"),
|
||||||
|
"实例文本的 -Xmx 必须生效:{args:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!args.iter().any(|arg| arg == "-Xmx2048m"),
|
||||||
|
"内存字段的 -Xmx 必须被抑制:{args:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolved = resolve(None, &GlobalLaunchDefaults::with_jvm_args("-Xmx3g"));
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
let args = argvs(&req);
|
||||||
|
assert!(
|
||||||
|
args.iter().any(|arg| arg == "-Xmx2048m"),
|
||||||
|
"全局文本的 -Xmx 不生效(第 7 项晚于第 5 项):{args:?}"
|
||||||
|
);
|
||||||
|
assert!(!args.iter().any(|arg| arg == "-Xmx3g"), "{args:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内存三级回落落进 `-Xmx`:实例 → 全局 → 默认。
|
||||||
|
#[test]
|
||||||
|
fn memory_falls_back_into_xmx() {
|
||||||
|
let global = GlobalLaunchDefaults {
|
||||||
|
memory_mb: Some(6144),
|
||||||
|
..GlobalLaunchDefaults::default()
|
||||||
|
};
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
memory_mb: Some(8192),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
for (overrides, expected) in [(Some(instance), "-Xmx8192m"), (None, "-Xmx6144m")] {
|
||||||
|
let resolved = resolve(overrides.as_ref(), &global);
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
assert!(
|
||||||
|
argvs(&req).iter().any(|arg| arg == expected),
|
||||||
|
"期望 {expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let resolved = resolve(None, &GlobalLaunchDefaults::default());
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
assert!(argvs(&req).iter().any(|arg| arg == "-Xmx2048m"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 游戏参数最后追加(可覆盖前面一切游戏参数)。
|
||||||
|
#[test]
|
||||||
|
fn game_args_land_at_the_tail() {
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
game_args: Some("--fullscreen --width 1920".to_string()),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
let args = argvs(&req);
|
||||||
|
let tail = &args[args.len() - 3..];
|
||||||
|
assert_eq!(tail, ["--fullscreen", "--width", "1920"], "{args:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **版本门槛**:26.2-snapshot-2+ 才下发;老版本不下发但必须给诊断告警(不静默)。
|
||||||
|
#[test]
|
||||||
|
fn graphics_backend_is_version_gated_with_a_warning() {
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
graphics_backend: Some(GraphicsBackend::Vulkan),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
||||||
|
|
||||||
|
let mut supported = request("26.2-snapshot-2");
|
||||||
|
apply(&mut supported, &resolved);
|
||||||
|
let command = assemble(&supported, &ctx()).expect("组装");
|
||||||
|
let at = position(&command.args, "--graphicsBackend").expect("支持版本要下发");
|
||||||
|
assert_eq!(command.args[at + 1], "vulkan");
|
||||||
|
assert!(
|
||||||
|
!command
|
||||||
|
.warnings
|
||||||
|
.iter()
|
||||||
|
.any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })),
|
||||||
|
"支持版本不该有跳过告警:{:?}",
|
||||||
|
command.warnings
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut old = request("1.20.4");
|
||||||
|
apply(&mut old, &resolved);
|
||||||
|
let command = assemble(&old, &ctx()).expect("组装");
|
||||||
|
assert!(
|
||||||
|
!command.args.iter().any(|arg| arg == "--graphicsBackend"),
|
||||||
|
"老版本不许下发:{:?}",
|
||||||
|
command.args
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
command
|
||||||
|
.warnings
|
||||||
|
.iter()
|
||||||
|
.any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })),
|
||||||
|
"跳过必须留诊断:{:?}",
|
||||||
|
command.warnings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `default` = 不下发、不告警(三态里的「跟随游戏默认」)。
|
||||||
|
#[test]
|
||||||
|
fn default_backend_emits_nothing() {
|
||||||
|
for instance in [None, Some(LaunchOverrides::default())] {
|
||||||
|
let resolved = resolve(instance.as_ref(), &GlobalLaunchDefaults::default());
|
||||||
|
let mut req = request("26.2");
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
let command = assemble(&req, &ctx()).expect("组装");
|
||||||
|
assert!(!command.args.iter().any(|arg| arg == "--graphicsBackend"));
|
||||||
|
assert!(command.warnings.is_empty(), "{:?}", command.warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动器自带的 `--fullscreen`(第 29 项)与实例游戏参数共存:实例在最后,可覆盖。
|
||||||
|
#[test]
|
||||||
|
fn instance_game_args_win_over_launcher_defaults() {
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
game_args: Some("--fullscreen".to_string()),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
req.fullscreen = true;
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
let args = argvs(&req);
|
||||||
|
assert_eq!(
|
||||||
|
args.iter()
|
||||||
|
.filter(|arg| arg.as_str() == "--fullscreen")
|
||||||
|
.count(),
|
||||||
|
2,
|
||||||
|
"两处都给 --fullscreen,由 MC 取后者(实例段在最后):{args:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(args.last().map(String::as_str), Some("--fullscreen"));
|
||||||
|
assert!(
|
||||||
|
position(&args, "net.minecraft.client.main.Main").unwrap() < args.len() - 1,
|
||||||
|
"实例游戏参数必须在主类之后"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Java 路径不进请求、只作为调用方的选择依据。
|
||||||
|
#[test]
|
||||||
|
fn java_path_is_a_scalar_for_the_caller() {
|
||||||
|
let instance = LaunchOverrides {
|
||||||
|
java_path: Some(PathBuf::from("D:/jdk21/bin/java.exe")),
|
||||||
|
..LaunchOverrides::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(Some(&instance), &GlobalLaunchDefaults::default());
|
||||||
|
assert_eq!(
|
||||||
|
java_binary(&resolved),
|
||||||
|
Some(Path::new("D:/jdk21/bin/java.exe"))
|
||||||
|
);
|
||||||
|
let mut req = request("1.20.4");
|
||||||
|
let before = req.java.binary.clone();
|
||||||
|
apply(&mut req, &resolved);
|
||||||
|
assert_eq!(req.java.binary, before, "apply 不改 Java 选择");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `backend_allowed`:继承解析后的库表 + 版本门限,两条判据的**唯一组合入口**。
|
||||||
|
///
|
||||||
|
/// 它是 UI 灰化与拼装链共用的那一格:调用方只负责把「继承解析后的版本 JSON」和
|
||||||
|
/// 「游戏版本号」递进来,不许自己拼 `has_vulkan_backend && is_at_least(..)`。
|
||||||
|
#[test]
|
||||||
|
fn backend_allowed_combines_capability_and_threshold() {
|
||||||
|
use crate::game::VersionJson;
|
||||||
|
|
||||||
|
let with_vulkan = VersionJson::from_json_str(
|
||||||
|
r#"{"id":"x","libraries":[{"name":"org.lwjgl:lwjgl-vulkan:3.4.3"}]}"#,
|
||||||
|
)
|
||||||
|
.expect("解析");
|
||||||
|
let without = VersionJson::from_json_str(r#"{"id":"x"}"#).expect("解析");
|
||||||
|
|
||||||
|
// 可解析版本:精确门限说了算(库表只作周快照兜底)。
|
||||||
|
assert!(backend_allowed(
|
||||||
|
&GameVersion::parse("26.2-snapshot-2"),
|
||||||
|
&without
|
||||||
|
));
|
||||||
|
assert!(!backend_allowed(
|
||||||
|
&GameVersion::parse("26.2-snapshot-1"),
|
||||||
|
&with_vulkan
|
||||||
|
));
|
||||||
|
assert!(!backend_allowed(
|
||||||
|
&GameVersion::parse("1.20.4"),
|
||||||
|
&with_vulkan
|
||||||
|
));
|
||||||
|
// 周快照:看库表。
|
||||||
|
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",
|
||||||
|
)];
|
||||||
|
|
||||||
|
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, "跳过与否必须与下发相反(不静默)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -199,11 +199,16 @@ pub fn build_request(
|
|||||||
.assets
|
.assets
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| resolved.asset_index.as_ref().map(|index| index.id.clone()));
|
.or_else(|| resolved.asset_index.as_ref().map(|index| index.id.clone()));
|
||||||
|
// 装配的门限 / 分支按**继承链的 MC 根版本**判,不是带加载器后缀的 version_id:
|
||||||
|
// `26w14a-fabric-0.16.5` 这种 id 在 `GameVersion::is_weekly_snapshot`(6 字符形态)上判不出来,
|
||||||
|
// 周快照的 `--graphicsBackend` 会在拼装链里被静默跳过——而详情页的门槛
|
||||||
|
// (`host::launch_args::backend_gate`)与 CLI 用的都是根版本,两边必须同一口径。
|
||||||
|
let (mc_version, _) = super::game::describe_version(env, &version_id);
|
||||||
let snapshot = SnapshotInput {
|
let snapshot = SnapshotInput {
|
||||||
version_id: &version_id,
|
version_id: &version_id,
|
||||||
version_name: Some(&target.name),
|
version_name: Some(&target.name),
|
||||||
version_type: None,
|
version_type: None,
|
||||||
game_version: None,
|
game_version: Some(mc_version.as_str()),
|
||||||
main_class: resolved.main_class.as_deref(),
|
main_class: resolved.main_class.as_deref(),
|
||||||
jvm_arguments: &expanded.jvm,
|
jvm_arguments: &expanded.jvm,
|
||||||
game_arguments: &expanded.game,
|
game_arguments: &expanded.game,
|
||||||
@@ -372,3 +377,6 @@ fn describe_warning(warning: &AdapterWarning) -> String {
|
|||||||
pub fn isolation_label(mode: IsolationMode) -> &'static str {
|
pub fn isolation_label(mode: IsolationMode) -> &'static str {
|
||||||
mode.as_str()
|
mode.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
//! `host::launch` 的回归用例(M157 r3 复审 P2-1):装配用的游戏版本必须是**继承链的 MC 根版本**。
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// 一次性临时目录(同 `host::launch_args` 测试:不引第三方 tempfile)。
|
||||||
|
struct TempDir(PathBuf);
|
||||||
|
|
||||||
|
impl TempDir {
|
||||||
|
fn new(tag: &str) -> Self {
|
||||||
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"ull-host-launch-{tag}-{}-{seq}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_dir_all(&path);
|
||||||
|
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_version(env: &HostEnv, id: &str, json: &str) {
|
||||||
|
let path = env.game.version_json(id);
|
||||||
|
std::fs::create_dir_all(path.parent().expect("版本目录")).expect("版本目录");
|
||||||
|
std::fs::write(&path, json).expect("写版本 JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **周快照 + 加载器实例**的装配:`SnapshotInput.game_version` 要拿继承链根的 `26w14a`,
|
||||||
|
/// 不是带加载器后缀的 `26w14a-fabric-0.16.5`。
|
||||||
|
///
|
||||||
|
/// 抓得住:`build_request` 回退成 `game_version: None`(装配链用 `version_id` 判门限)——
|
||||||
|
/// 那时 `GameVersion::is_weekly_snapshot` 的 6 字符形态认不出这个 id,`--graphicsBackend`
|
||||||
|
/// 会在拼装链里被静默跳过,而详情页的门槛(`host::launch_args::backend_gate`,用根版本)
|
||||||
|
/// 显示「可用」——正是复审复现的「UI 说能开、命令行里没有」。
|
||||||
|
/// 抓不住:拼装本身(`crates/core/src/launch/overrides/tests.rs` 的
|
||||||
|
/// `modded_weekly_snapshot_needs_the_root_version_for_the_gate` 出完整 argv)。
|
||||||
|
#[test]
|
||||||
|
fn weekly_loader_instance_assembles_against_the_mc_root_version() {
|
||||||
|
let dir = TempDir::new("weekly");
|
||||||
|
let env = HostEnv::at_root(dir.path());
|
||||||
|
std::fs::create_dir_all(env.layout.config()).expect("config 目录");
|
||||||
|
|
||||||
|
// 周快照本体:库表带 lwjgl-vulkan(能力位),主类齐全。
|
||||||
|
write_version(
|
||||||
|
&env,
|
||||||
|
"26w14a",
|
||||||
|
r#"{"id":"26w14a","type":"snapshot","mainClass":"net.minecraft.client.main.Main",
|
||||||
|
"libraries":[{"name":"org.lwjgl:lwjgl-vulkan:3.4.3"}]}"#,
|
||||||
|
);
|
||||||
|
// Fabric 层:只写自己的主类 + inheritsFrom(真实落盘形状)。
|
||||||
|
write_version(
|
||||||
|
&env,
|
||||||
|
"26w14a-fabric-0.16.5",
|
||||||
|
r#"{"id":"26w14a-fabric-0.16.5","inheritsFrom":"26w14a",
|
||||||
|
"mainClass":"net.fabricmc.loader.impl.launch.knot.KnotClient"}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let target = LaunchTarget {
|
||||||
|
id: "inst-1".to_string(),
|
||||||
|
name: "周快照 Fabric".to_string(),
|
||||||
|
version_id: "26w14a-fabric-0.16.5".to_string(),
|
||||||
|
account: None,
|
||||||
|
};
|
||||||
|
let (request, _warnings) = build_request(&env, &target).expect("装配应成功");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
request.instance.version.raw(),
|
||||||
|
"26w14a",
|
||||||
|
"装配用的游戏版本必须是继承链的 MC 根版本(带加载器后缀的 id 会让周快照门限失效)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
request.instance.version.is_weekly_snapshot(),
|
||||||
|
"根版本要能被识别成周快照形态"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
request.manifest.has_vulkan_backend,
|
||||||
|
"能力位来自继承链解析后的库表(lwjgl-vulkan 在父层)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!request.instance.version.is_known(),
|
||||||
|
"周快照刻意不可排序(is_known=false),门限只能靠库表兜底"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -215,10 +215,7 @@ mod tests {
|
|||||||
// (并行跑就是「系统找不到指定的路径」——M157 r2 实测)。
|
// (并行跑就是「系统找不到指定的路径」——M157 r2 实测)。
|
||||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
let path = std::env::temp_dir().join(format!(
|
let path = std::env::temp_dir().join(format!("{tag}-{}-{seq}", std::process::id()));
|
||||||
"{tag}-{}-{seq}",
|
|
||||||
std::process::id()
|
|
||||||
));
|
|
||||||
let _ = std::fs::remove_dir_all(&path);
|
let _ = std::fs::remove_dir_all(&path);
|
||||||
std::fs::create_dir_all(&path).expect("建临时目录");
|
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||||
Self { path }
|
Self { path }
|
||||||
|
|||||||
Reference in new issue
Block a user