fix(coexist): r2 返工——帮助写真实例路径 + 加载器安装也说出「原版清单被替换」+ --json 给出绑定成败(M216)
r2 复审两条 P2 必修 + 一条「能一行接上就接」的 P3。
P2-1(`ull help pack` 的落点少一层 `.ULL/data`)
- 「数据根」在这份帮助自己的定义里是「含 `.ULL/` 与 `.minecraft/` 的父目录」(`--data-dir` 那行),
所以真路径是 `<数据根>/.ULL/data/instances/<实例目录名>`。r1 写成 `<数据根>/instances/<实例名>`,
照读会去一个不存在的位置找包内容。
- help.rs 与 model.rs 的字段文档改成真路径,并点明目录名是归一化后的结果(`Better-MC`)。
- 用例改成 `help_describes_the_new_default_target_with_the_real_path`:先断言真路径整串在、
再断言 `<数据根>/instances/` **不在**(这一条就是抓「少写 .ULL/data」那种错法)。
P2-2(装加载器时原版那一步的「替换了清单」没人说)
- `loader.rs`:`plan.vanilla_install_needed` 那条路以前只拿 `vanilla_report` 判 `is_complete()`
就丢掉;现在把它留在 `vanilla_report` 里传给 `report_loader::build`。
- `report_loader.rs`:`build` / `human_lines` 各多一个 `vanilla: Option<&InstallReport>`;
新增 `vanilla_lines`(人读:`告警(原版): …` + `原版版本清单已替换(旧的那份备份在 …)`)与
`vanilla_json`(`--json` 的 `vanilla` 段:warnings / version_json / manifest{kind,backup})。
- 用例 `a_replaced_vanilla_manifest_is_said_out_loud_in_the_loader_output`:用真
`LoaderInstallPlan`(最小 Fabric,零网络)调 `human_lines`,断言告警与备份路径都在;
未先装原版时不多说话;`vanilla_json` 的 `kind`/`backup` 结构正确、`None` 时没有该段。
P3(复审「能一行接上就接」):`--json` 给出导入绑定的**结构化成败**
- core `pack_dir::bind_created_instance` 返回 `Option<Result<String, String>>`(None=不需要绑 /
Ok=已绑 / Err=绑失败),一句话就能分出成败,不必猜文案。
- CLI:`outcome` 多写一个 `"instance_bound": true|false`(人读行照旧给原因);
GUI:`import.rs` 按 Ok/Err 分别记 `instance_bound` / `instance_binding_failed` 注记。
- 端到端用例里补断言 `data.instance_bound == true`。
This commit is contained in:
1 parent
4e1323212b
commit
aa61565fe7
8 files changed
+188
-27
No files matched your search
@@ -104,8 +104,9 @@ pub fn topic(name: &str) -> Option<&'static str> {
|
|||||||
MultiMC mmc-pack.json(formatVersion 必须为 1)+ instance.cfg + .minecraft/
|
MultiMC mmc-pack.json(formatVersion 必须为 1)+ instance.cfg + .minecraft/
|
||||||
(Prism 实例;非加载器 component 会逐项列进报告,不静默丢)
|
(Prism 实例;非加载器 component 会逐项列进报告,不静默丢)
|
||||||
|
|
||||||
目标目录缺省 = 新实例目录 `<数据根>/instances/<实例名>`(并登记实例,M216 起不再落共享
|
目标目录缺省 = 新实例目录 `<数据根>/.ULL/data/instances/<实例目录名>`(并登记实例,M216 起
|
||||||
.minecraft 根);`--target` 可钉到别处(例如某个实例的运行目录)。
|
不再落共享 .minecraft 根;目录名是包名/`--name` 经 ASCII 归一化后的结果,如 `Better-MC`);
|
||||||
|
`--target` 可钉到别处(例如某个实例的运行目录)。
|
||||||
两阶段落盘:先 overrides(三方合并:包内原值 / 磁盘现状 / 上次基线),再补远端文件。
|
两阶段落盘:先 overrides(三方合并:包内原值 / 磁盘现状 / 上次基线),再补远端文件。
|
||||||
· 用户改过的文件不会被静默覆盖(默认保留,逐条进冲突清单);`--overwrite` 才用包覆盖
|
· 用户改过的文件不会被静默覆盖(默认保留,逐条进冲突清单);`--overwrite` 才用包覆盖
|
||||||
· 缺文件(断网 / CurseForge files[] 补不出来)可容忍,但结果降级为「部分成功」
|
· 缺文件(断网 / CurseForge files[] 补不出来)可容忍,但结果降级为「部分成功」
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ pub struct InstallArgs {
|
|||||||
pub struct PackImportArgs {
|
pub struct PackImportArgs {
|
||||||
/// 整合包路径:`.zip` / `.mrpack` / MultiMC 实例目录。
|
/// 整合包路径:`.zip` / `.mrpack` / MultiMC 实例目录。
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
/// 目标实例目录(缺省 = 新实例目录 `<数据根>/instances/<实例名>`,M216 前是共享游戏目录
|
/// 目标实例目录(缺省 = 新实例目录 `<数据根>/.ULL/data/instances/<实例目录名>`,M216 前是
|
||||||
/// `game_home`)。
|
/// 共享游戏目录 `game_home`)。
|
||||||
pub target: Option<PathBuf>,
|
pub target: Option<PathBuf>,
|
||||||
/// 实例显示名(缺省 = 包名)。
|
/// 实例显示名(缺省 = 包名)。
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|||||||
@@ -85,12 +85,13 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
|||||||
skipped: false,
|
skipped: false,
|
||||||
});
|
});
|
||||||
return Ok(report_loader::build(
|
return Ok(report_loader::build(
|
||||||
ctx, &engine, now, true, &plan, None, None, &name, section,
|
ctx, &engine, now, true, &plan, None, None, None, &name, section,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut vanilla_report = None;
|
||||||
if plan.vanilla_install_needed {
|
if plan.vanilla_install_needed {
|
||||||
let (_, vanilla_report) = vanilla::plan_and_install(
|
let (_, report) = vanilla::plan_and_install(
|
||||||
ctx,
|
ctx,
|
||||||
&engine,
|
&engine,
|
||||||
&version,
|
&version,
|
||||||
@@ -98,11 +99,14 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
|||||||
args.progress,
|
args.progress,
|
||||||
args.json_progress,
|
args.json_progress,
|
||||||
)?;
|
)?;
|
||||||
if let Some(report) = &vanilla_report {
|
if let Some(report) = &report {
|
||||||
if !report.is_complete() {
|
if !report.is_complete() {
|
||||||
return Err(failure_error(report, ctx));
|
return Err(failure_error(report, ctx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **M216 r2 P2-2**:原版那段 report 不能在这里丢掉——它的 `warnings` / `manifest`
|
||||||
|
// (可能替换了共享 `.minecraft` 里 `versions/<原版>/<原版>.json`)必须进人读与 `--json`。
|
||||||
|
vanilla_report = report;
|
||||||
}
|
}
|
||||||
|
|
||||||
// installer processors 段:版本 JSON 已在盘上 = 上一轮早已跑完,整段跳过。
|
// installer processors 段:版本 JSON 已在盘上 = 上一轮早已跑完,整段跳过。
|
||||||
@@ -143,6 +147,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
|||||||
false,
|
false,
|
||||||
&plan,
|
&plan,
|
||||||
Some(&install_report),
|
Some(&install_report),
|
||||||
|
vanilla_report.as_ref(),
|
||||||
Some(®istration),
|
Some(®istration),
|
||||||
&name,
|
&name,
|
||||||
section,
|
section,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! 与 [`super::report`] 的分工:那边是"原版安装"(客户端 jar + 资源 + 库),这边是
|
//! 与 [`super::report`] 的分工:那边是"原版安装"(客户端 jar + 资源 + 库),这边是
|
||||||
//! "叠在原版上的 loader 层"(只有库)。共用同一套进度回调与字节格式化。
|
//! "叠在原版上的 loader 层"(只有库)。共用同一套进度回调与字节格式化。
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
use unlucky_core::addon::loader::processors::{ProcessorPlan, ProcessorRun};
|
use unlucky_core::addon::loader::processors::{ProcessorPlan, ProcessorRun};
|
||||||
use unlucky_core::addon::loader::LoaderInstallPlan;
|
use unlucky_core::addon::loader::LoaderInstallPlan;
|
||||||
use unlucky_core::download::DownloadEngine;
|
use unlucky_core::download::DownloadEngine;
|
||||||
@@ -39,6 +39,7 @@ pub fn build(
|
|||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
plan: &LoaderInstallPlan,
|
plan: &LoaderInstallPlan,
|
||||||
report: Option<&InstallReport>,
|
report: Option<&InstallReport>,
|
||||||
|
vanilla: Option<&InstallReport>,
|
||||||
registration: Option<&Registration>,
|
registration: Option<&Registration>,
|
||||||
name: &str,
|
name: &str,
|
||||||
processors: Option<ProcessorSection<'_>>,
|
processors: Option<ProcessorSection<'_>>,
|
||||||
@@ -78,6 +79,9 @@ pub fn build(
|
|||||||
if let Some(section) = &processors {
|
if let Some(section) = &processors {
|
||||||
data["processors"] = processor_json(section);
|
data["processors"] = processor_json(section);
|
||||||
}
|
}
|
||||||
|
if let Some(block) = vanilla_json(vanilla) {
|
||||||
|
data["vanilla"] = block;
|
||||||
|
}
|
||||||
|
|
||||||
Report::new(
|
Report::new(
|
||||||
data,
|
data,
|
||||||
@@ -85,6 +89,7 @@ pub fn build(
|
|||||||
dry_run,
|
dry_run,
|
||||||
plan,
|
plan,
|
||||||
report,
|
report,
|
||||||
|
vanilla,
|
||||||
registration,
|
registration,
|
||||||
&samples,
|
&samples,
|
||||||
name,
|
name,
|
||||||
@@ -115,10 +120,47 @@ fn processor_json(section: &ProcessorSection<'_>) -> serde_json::Value {
|
|||||||
node
|
node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `--json` 的 `vanilla` 段:先装原版那一步的告警 + 清单结局(`None` = 这次没先装原版)。
|
||||||
|
///
|
||||||
|
/// **M216 r2 P2-2**:原版 report 在 `is_complete()` 之后就被丢了 ⇒ 「替换了共享 `.minecraft`
|
||||||
|
/// 里的原版清单」在加载器安装的 JSON 里没有痕迹。
|
||||||
|
fn vanilla_json(vanilla: Option<&InstallReport>) -> Option<Value> {
|
||||||
|
let report = vanilla?;
|
||||||
|
Some(json!({
|
||||||
|
"warnings": report.warnings,
|
||||||
|
"version_json": report.version_json.as_ref().map(|path| path.display().to_string()),
|
||||||
|
"manifest": report.manifest.as_ref().map(|write| json!({
|
||||||
|
"kind": write.as_str(),
|
||||||
|
"backup": write.backup().map(|path| path.display().to_string()),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 人读行里「先装原版」那一段(**M216 r2 P2-2**,与 [`vanilla_json`] 同源):以前
|
||||||
|
/// `human_lines` 只遍历 `plan.warnings()`,原版那份 report 的告警与「清单被替换」都没说。
|
||||||
|
fn vanilla_lines(vanilla: Option<&InstallReport>) -> Vec<String> {
|
||||||
|
let Some(report) = vanilla else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let mut lines: Vec<String> = report
|
||||||
|
.warnings
|
||||||
|
.iter()
|
||||||
|
.map(|warning| format!("告警(原版): {warning}"))
|
||||||
|
.collect();
|
||||||
|
if let Some(backup) = report.manifest.as_ref().and_then(|write| write.backup()) {
|
||||||
|
lines.push(format!(
|
||||||
|
"原版版本清单已替换(旧的那份备份在 {})",
|
||||||
|
backup.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
fn human_lines(
|
fn human_lines(
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
plan: &LoaderInstallPlan,
|
plan: &LoaderInstallPlan,
|
||||||
report: Option<&InstallReport>,
|
report: Option<&InstallReport>,
|
||||||
|
vanilla: Option<&InstallReport>,
|
||||||
registration: Option<&Registration>,
|
registration: Option<&Registration>,
|
||||||
samples: &[serde_json::Value],
|
samples: &[serde_json::Value],
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -181,6 +223,7 @@ fn human_lines(
|
|||||||
for warning in plan.warnings() {
|
for warning in plan.warnings() {
|
||||||
human.push(format!("告警: {warning}"));
|
human.push(format!("告警: {warning}"));
|
||||||
}
|
}
|
||||||
|
human.extend(vanilla_lines(vanilla));
|
||||||
if let Some(section) = processors {
|
if let Some(section) = processors {
|
||||||
human.extend(processor_lines(section));
|
human.extend(processor_lines(section));
|
||||||
}
|
}
|
||||||
@@ -272,3 +315,85 @@ fn processor_lines(section: &ProcessorSection<'_>) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use unlucky_core::addon::loader::{
|
||||||
|
LoaderInstallPlan, LoaderKind, LoaderPlanInput, LoaderRelease, LoaderSupport,
|
||||||
|
};
|
||||||
|
use unlucky_core::game::arguments::Environment;
|
||||||
|
use unlucky_core::game::install::ManifestWrite;
|
||||||
|
use unlucky_core::game::layout::GameLayout;
|
||||||
|
use unlucky_core::game::manifest::VersionJson;
|
||||||
|
|
||||||
|
/// 一份最小的 Fabric 加载器计划(零网络:只算路径,不落盘)。
|
||||||
|
fn loader_plan(root: &std::path::Path) -> LoaderInstallPlan {
|
||||||
|
let layout = GameLayout::new(root);
|
||||||
|
let release = LoaderRelease {
|
||||||
|
kind: LoaderKind::Fabric,
|
||||||
|
mc_version: "1.20.1".to_string(),
|
||||||
|
loader_version: "0.15.11".to_string(),
|
||||||
|
version_id: "1.20.1-fabric-0.15.11".to_string(),
|
||||||
|
version_json: VersionJson::from_json_str(
|
||||||
|
r#"{"id":"1.20.1-fabric-0.15.11","mainClass":"net.fabricmc.loader.impl.launch.knot.KnotClient","inheritsFrom":"1.20.1","libraries":[]}"#,
|
||||||
|
)
|
||||||
|
.expect("loader 清单应可解析"),
|
||||||
|
support: LoaderSupport::JsonRoute,
|
||||||
|
installer: None,
|
||||||
|
install_profile: None,
|
||||||
|
warnings: Vec::new(),
|
||||||
|
};
|
||||||
|
LoaderInstallPlan::build(LoaderPlanInput {
|
||||||
|
layout: &layout,
|
||||||
|
release: &release,
|
||||||
|
environment: &Environment::host(),
|
||||||
|
vanilla_installed: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **M216 r2 P2-2**:装加载器时「先装原版」那一步替换了共享 `.minecraft` 里的原版清单,
|
||||||
|
/// 这句话必须出现在**加载器安装**的人读输出里(此前只出现在纯原版安装的输出里)。
|
||||||
|
///
|
||||||
|
/// 抓得住:`human_lines` 只遍历 `plan.warnings()`(= r1 状态);备份路径没渲出来;
|
||||||
|
/// `--json` 的 `vanilla` 段没了。抓不住:真实 `ull install --loader …` 整条链路(要网络),
|
||||||
|
/// 以及「原版 report 是否真被 `loader.rs` 传进来」那步接线(见报告)。
|
||||||
|
#[test]
|
||||||
|
fn a_replaced_vanilla_manifest_is_said_out_loud_in_the_loader_output() {
|
||||||
|
let root = std::env::temp_dir().join(format!("ull216-loader-{}", std::process::id()));
|
||||||
|
let plan = loader_plan(&root);
|
||||||
|
let backup = PathBuf::from("/g/.minecraft/versions/1.20.1/1.20.1.json.ullbak-99");
|
||||||
|
let vanilla = InstallReport {
|
||||||
|
files: Vec::new(),
|
||||||
|
version_json: Some(PathBuf::from("/g/.minecraft/versions/1.20.1/1.20.1.json")),
|
||||||
|
manifest: Some(ManifestWrite::Replaced {
|
||||||
|
backup: backup.clone(),
|
||||||
|
}),
|
||||||
|
warnings: vec!["已替换版本清单 /g/.minecraft/versions/1.20.1/1.20.1.json".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = human_lines(false, &plan, None, Some(&vanilla), None, &[], "n", None).join("\n");
|
||||||
|
assert!(
|
||||||
|
text.contains("已替换版本清单"),
|
||||||
|
"原版那段的告警没进人读输出:{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains(&backup.display().to_string()),
|
||||||
|
"原版清单的备份路径没进人读输出:{text}"
|
||||||
|
);
|
||||||
|
// 没先装原版时不许凭空多说话。
|
||||||
|
let plain = human_lines(false, &plan, None, None, None, &[], "n", None).join("\n");
|
||||||
|
assert!(!plain.contains("原版版本清单已替换"), "{plain}");
|
||||||
|
|
||||||
|
// `--json` 的 `vanilla` 段(结构化,脚本可读)。
|
||||||
|
let block = vanilla_json(Some(&vanilla)).expect("应当有 vanilla 段");
|
||||||
|
assert_eq!(block["manifest"]["kind"], Value::String("replaced".into()));
|
||||||
|
assert_eq!(
|
||||||
|
block["manifest"]["backup"].as_str(),
|
||||||
|
Some(backup.to_string_lossy().as_ref())
|
||||||
|
);
|
||||||
|
assert!(vanilla_json(None).is_none());
|
||||||
|
std::fs::remove_dir_all(&root).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ pub fn run(ctx: &Ctx, args: &PackImportArgs) -> Result<Outcome, CliError> {
|
|||||||
&target,
|
&target,
|
||||||
&seen,
|
&seen,
|
||||||
args,
|
args,
|
||||||
binding.as_deref(),
|
binding.as_ref(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ fn outcome(
|
|||||||
target: &Path,
|
target: &Path,
|
||||||
stages: &[&'static str],
|
stages: &[&'static str],
|
||||||
args: &PackImportArgs,
|
args: &PackImportArgs,
|
||||||
binding: Option<&str>,
|
binding: Option<&Result<String, String>>,
|
||||||
) -> Outcome {
|
) -> Outcome {
|
||||||
let mut data = serde_json::to_value(report).unwrap_or_else(|_| {
|
let mut data = serde_json::to_value(report).unwrap_or_else(|_| {
|
||||||
json!({
|
json!({
|
||||||
@@ -144,8 +144,14 @@ fn outcome(
|
|||||||
if !stages.is_empty() {
|
if !stages.is_empty() {
|
||||||
human.push(format!("阶段:{}", stages.join(" → ")));
|
human.push(format!("阶段:{}", stages.join(" → ")));
|
||||||
}
|
}
|
||||||
if let Some(note) = binding {
|
if let Some(binding) = binding {
|
||||||
human.push(note.to_string());
|
// **M216 r2**:`--json` 里给**结构化成败**(r1 只印人读行,脚本看不到成败)。
|
||||||
|
if let Some(object) = data.as_object_mut() {
|
||||||
|
object.insert("instance_bound".to_string(), json!(binding.is_ok()));
|
||||||
|
}
|
||||||
|
human.push(match binding {
|
||||||
|
Ok(note) | Err(note) => note.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
human.push(format!(
|
human.push(format!(
|
||||||
"结果:{} 文件 新{} / 覆盖{} / 跳过{} / 保留{} 冲突 {} 缺文件 {}",
|
"结果:{} 文件 新{} / 覆盖{} / 跳过{} / 保留{} 冲突 {} 缺文件 {}",
|
||||||
|
|||||||
@@ -221,6 +221,14 @@ fn the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it() {
|
|||||||
]);
|
]);
|
||||||
assert_eq!(code, 0, "stdout={stdout} stderr={stderr}");
|
assert_eq!(code, 0, "stdout={stdout} stderr={stderr}");
|
||||||
|
|
||||||
|
// **M216 r2**:绑定成败也进 `--json`(结构化,不必猜文案)。
|
||||||
|
let value = parse_json(&stdout);
|
||||||
|
assert_eq!(
|
||||||
|
value["data"]["instance_bound"],
|
||||||
|
Value::Bool(true),
|
||||||
|
"缺省导入应当把实例绑到内容目录并报 true:{stdout}"
|
||||||
|
);
|
||||||
|
|
||||||
let instance_dir = data.path().join(".ULL/data/instances/My-Pack");
|
let instance_dir = data.path().join(".ULL/data/instances/My-Pack");
|
||||||
assert!(
|
assert!(
|
||||||
instance_dir.join("mods/a.jar").is_file(),
|
instance_dir.join("mods/a.jar").is_file(),
|
||||||
@@ -266,18 +274,27 @@ fn the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **M216 P2-1**:`--help` 里的缺省落点必须与新行为一致(写旧行为 = 用户照读会以为文件进了
|
/// **M216 P2-1 / r2**:`--help` 里的缺省落点必须是**真路径**。
|
||||||
/// `.minecraft`)。
|
|
||||||
///
|
///
|
||||||
/// 抓得住:文案退回「共享游戏目录」;新落点没写进去。抓不住:`model.rs` 的字段文档
|
/// 「数据根」在这份帮助自己的定义里是「含 `.ULL/` 与 `.minecraft/` 的父目录」(`--data-dir` 那行),
|
||||||
/// (那是编译期文档,测试读不到)——所以断言落在这里只覆盖用户能看见的那一份。
|
/// 所以实例目录的真路径是 `<数据根>/.ULL/data/instances/<目录名>`。r1 写成了
|
||||||
|
/// `<数据根>/instances/<实例名>` —— 少一层 `.ULL/data`,照读会去一个不存在的位置找包内容。
|
||||||
|
///
|
||||||
|
/// 抓得住:缺 `.ULL/data` 这一层(下面第二条断言就是抓它的——少写时该子串恰好出现);
|
||||||
|
/// 文案退回「共享游戏目录」;目录名归一化这件事没提。抓不住:`model.rs` 的字段文档
|
||||||
|
/// (编译期文档,测试读不到),以及真实 `--data-dir` 下的绝对路径拼接(那由上面那条端到端
|
||||||
|
/// 用例按真目录断言)。
|
||||||
#[test]
|
#[test]
|
||||||
fn help_describes_the_new_default_target() {
|
fn help_describes_the_new_default_target_with_the_real_path() {
|
||||||
let (code, stdout, _) = run_cli(&["help", "pack"]);
|
let (code, stdout, _) = run_cli(&["help", "pack"]);
|
||||||
assert_eq!(code, 0);
|
assert_eq!(code, 0);
|
||||||
assert!(
|
assert!(
|
||||||
stdout.contains("instances/<实例名>"),
|
stdout.contains("<数据根>/.ULL/data/instances/<实例目录名>"),
|
||||||
"帮助里没写新缺省落点:{stdout}"
|
"帮助里没写真路径 <数据根>/.ULL/data/instances/<实例目录名>:{stdout}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!stdout.contains("<数据根>/instances/"),
|
||||||
|
"帮助里的落点少了 `.ULL/data` 这一层(r2 抓的就是这个错法):{stdout}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!stdout.contains("缺省 = 数据根的共享游戏目录"),
|
!stdout.contains("缺省 = 数据根的共享游戏目录"),
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ pub fn bind_instance_dir(
|
|||||||
|
|
||||||
/// 导入收尾:把「我们新建出来的实例」绑到它的内容目录,返回一句给人读的结论。
|
/// 导入收尾:把「我们新建出来的实例」绑到它的内容目录,返回一句给人读的结论。
|
||||||
///
|
///
|
||||||
|
/// 返回 `None` = 不需要绑;`Some(Ok(说明))` = 已绑;`Some(Err(说明))` = 绑失败(档案在、内容在,
|
||||||
|
/// 只是运行目录没指过来——调用方**别**把它当整次导入失败,但要让人看见)。
|
||||||
|
///
|
||||||
/// 判据(CLI 与 GUI 共用一份,别自己再写一遍):
|
/// 判据(CLI 与 GUI 共用一份,别自己再写一遍):
|
||||||
/// - `auto_bound == false` ⇒ 不绑(用户显式给了目标目录 / `--no-register`:那目录可能是别人的
|
/// - `auto_bound == false` ⇒ 不绑(用户显式给了目标目录 / `--no-register`:那目录可能是别人的
|
||||||
/// 运行目录,也可能被用户自己指过);
|
/// 运行目录,也可能被用户自己指过);
|
||||||
@@ -146,23 +149,23 @@ pub fn bind_created_instance(
|
|||||||
target: &Path,
|
target: &Path,
|
||||||
auto_bound: bool,
|
auto_bound: bool,
|
||||||
report: &crate::addon::import::ImportReport,
|
report: &crate::addon::import::ImportReport,
|
||||||
) -> Option<String> {
|
) -> Option<Result<String, String>> {
|
||||||
if !auto_bound {
|
if !auto_bound {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let registered = report.registered.as_ref().filter(|item| item.created)?;
|
let registered = report.registered.as_ref().filter(|item| item.created)?;
|
||||||
Some(
|
Some(
|
||||||
match bind_instance_dir(layout, ®istered.id, ®istered.version_id, target) {
|
match bind_instance_dir(layout, ®istered.id, ®istered.version_id, target) {
|
||||||
Ok(()) => format!(
|
Ok(()) => Ok(format!(
|
||||||
"运行目录已指向 {}(实例 {})",
|
"运行目录已指向 {}(实例 {})",
|
||||||
target.display(),
|
target.display(),
|
||||||
registered.name
|
registered.name
|
||||||
),
|
)),
|
||||||
// 内容与档案都已就位,只是目录没指过来 ⇒ 不把整次导入判失败,但必须说出来。
|
// 内容与档案都已就位,只是目录没指过来 ⇒ 不把整次导入判失败,但必须说出来。
|
||||||
Err(error) => format!(
|
Err(error) => Err(format!(
|
||||||
"实例已登记但没能绑到导入目录({error}):启动前请把该实例的运行目录指到 {}",
|
"实例已登记但没能绑到导入目录({error}):启动前请把该实例的运行目录指到 {}",
|
||||||
target.display()
|
target.display()
|
||||||
),
|
)),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,10 +202,14 @@ pub(super) fn run(
|
|||||||
// **§5**:导入建出来的实例要绑到它的内容目录(`isolation=custom` + `gameDir`),否则运行
|
// **§5**:导入建出来的实例要绑到它的内容目录(`isolation=custom` + `gameDir`),否则运行
|
||||||
// 目录由全局档派生成 `<game>/versions/<version_id>`,这批 mods/config 永远不会被启动用到。
|
// 目录由全局档派生成 `<game>/versions/<version_id>`,这批 mods/config 永远不会被启动用到。
|
||||||
// 判据与文案在 core(与 CLI 同一份);失败不把整次导入判失败,但要让用户看见。
|
// 判据与文案在 core(与 CLI 同一份);失败不把整次导入判失败,但要让用户看见。
|
||||||
if let Some(note) = pack_dir::bind_created_instance(&env.layout, &target, register, &report) {
|
match pack_dir::bind_created_instance(&env.layout, &target, register, &report) {
|
||||||
report
|
Some(Ok(note)) => report
|
||||||
.warnings
|
.warnings
|
||||||
.push(ImportIssue::new("instance_binding", note));
|
.push(ImportIssue::new("instance_bound", note)),
|
||||||
|
Some(Err(note)) => report
|
||||||
|
.warnings
|
||||||
|
.push(ImportIssue::new("instance_binding_failed", note)),
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
// 取消标记要**在管线返回之后**再读一次:取消可能来得太晚(管线已经跑完),这一读就是
|
// 取消标记要**在管线返回之后**再读一次:取消可能来得太晚(管线已经跑完),这一读就是
|
||||||
// "太晚"的唯一证据(`finish` 用它把报告标成 `TooLate`)。
|
// "太晚"的唯一证据(`finish` 用它把报告标成 `TooLate`)。
|
||||||
|
|||||||
Reference in new issue
Block a user