fix(coexist): 复审 r1 返工——帮助文案跟上新落点 + 「替换了清单」进人读通道 + 导入建实例绑到内容目录(M216 P2/P3/§5)

P2-1 ② 改了缺省落点,CLI 自己的文档还在写旧行为
- `cli/help.rs`「目标目录缺省 = 数据根的共享游戏目录(.minecraft/)」→ 新实例目录
  `<数据根>/instances/<实例名>`(并登记实例);
- `cli/model.rs` 的 `PackImportArgs::target` 字段文档同步。
- 用例 `help_describes_the_new_default_target`(断言帮助含新落点、不含旧落点)。

P2-2 ③ 的「显式说明」进不了任何人读通道
- CLI:`human_lines` 原只遍历 `plan.warnings`,而运行期告警(含「已替换版本清单 …旧的那份
  备份在 …」)在 `report.warnings` 里 ⇒ `ull install` 人读输出从没说过这句话。现在有 report 就
  报 report.warnings(它开头就是 plan 告警的副本),`manifest=Replaced` 再单独打一行备份路径。
- GUI:`install.rs` 新增 `Job::journal_report`(把 `report.warnings` 与 `manifest` 的备份路径写进
  安装日志),`install/routes.rs` 两处 `ensure_complete` 之前各加一行调用(tower 批的两行)。
  此前 `report.manifest` 全仓零消费者。
- 用例 `a_replaced_manifest_is_said_out_loud_in_the_human_output`(走真 `InstallPlan` 排版)。

P3-1 缺省目录被占的 hint 删掉不成立的 `--overwrite` 那半句(它绕过不了这条预检)。
P3-2 GUI 落点改成委托 core 的 `pack_dir::instance_dir`(此前 presenter 自己拼 `data()/instances/…`);
`convert::dir_name` 缩成 `#[cfg(test)]` 的委托入口(生产侧已无调用点,留着继续钉归一化判据)。
P3-3 补 `the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it`:不带 `--target`
跑通整条 CLI,断言落点、内容、登记、以及实例设置里的 `isolation`/`gameDir`。

§5 导入建出来的实例原先「导入成功但内容永远不会被启动用到」
- 新增 `pack_dir::bind_instance_dir`(`InstanceConfig.isolation = Custom` + `game_dir = 导入目录`)
  与 `pack_dir::bind_created_instance`(判据+文案,CLI 与 GUI 同一份);
- CLI 在缺省建实例后、GUI 在 `import.rs::run` 里各调一次;只在「我们新建了实例目录且档案是新建」
  时绑(显式 `--target` / `--no-register` / 复用已有档案都不动);
- 运行目录 = 导入目录(saves/mods/logs/config),**versions/libraries/assets 仍按 D20 恒共享**;
- 未改 `derive_game_dir`。
- 用例:正向 `binding_an_imported_instance_points_it_at_the_import_dir`、反向
  `a_freshly_registered_instance_still_inherits_the_global_mode`。
This commit is contained in:
AskaEth committed 2026-11-09 19:46:41 +08:00
1 parent 96f15c79fb
commit 4e1323212b
10 files changed
+359 -45

No files matched your search

@@ -343,10 +343,14 @@ pub(super) fn pack_stem(pack: &Path) -> String {
unlucky_core::game::install::pack_dir::pack_stem(pack)
}
/// **实例目录名**(判据见 core 的 [`pack_dir::instance_dir_name`]):本函数现在只有委托,
/// 保留它是因为 presenter 侧五个调用点按 `convert::dir_name` 这个名字取。
/// **实例目录名**(判据见 core 的 [`pack_dir::instance_dir_name`]):本函数只有一行委托。
///
/// **只剩 presenter 自己的用例在调它**(生产侧落点已改成 `pack_dir::instance_dir`,见 M216
/// P3-2),所以挂 `cfg(test)`:它是那组既有判据(空格折 `-`、中文补 `-<hash8>`、两个中文名
/// 不塌成同一个目录)继续钉住归一化规则的入口,生产构建里不需要它。
///
/// [`pack_dir::instance_dir_name`]: unlucky_core::game::install::pack_dir::instance_dir_name
#[cfg(test)]
pub(super) fn dir_name(name: &str) -> String {
unlucky_core::game::install::pack_dir::instance_dir_name(name)
}
+14 -6
View File
@@ -16,11 +16,13 @@ use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use unlucky_core::addon::import::report::ImportIssue;
use unlucky_core::addon::import::{
import_pack, scan_pack_online, DownloadEngineFetcher, ImportCancel, ImportProgress,
ImportProgressSink, ImportRequest,
};
use unlucky_core::download::blocking::block_on;
use unlucky_core::game::install::pack_dir;
use crate::dto::{ImportRequestView, PackEvent};
use unlucky_core::addon::import::ImportStatus;
@@ -196,7 +198,15 @@ pub(super) fn run(
core_request = core_request.with_registration(&env.layout, Some(&pack_name));
}
let report = block_on(import_pack(&core_request));
let mut report = block_on(import_pack(&core_request));
// **§5**:导入建出来的实例要绑到它的内容目录(`isolation=custom` + `gameDir`),否则运行
// 目录由全局档派生成 `<game>/versions/<version_id>`,这批 mods/config 永远不会被启动用到。
// 判据与文案在 core(与 CLI 同一份);失败不把整次导入判失败,但要让用户看见。
if let Some(note) = pack_dir::bind_created_instance(&env.layout, &target, register, &report) {
report
.warnings
.push(ImportIssue::new("instance_binding", note));
}
// 取消标记要**在管线返回之后**再读一次:取消可能来得太晚(管线已经跑完),这一读就是
// "太晚"的唯一证据(`finish` 用它把报告标成 `TooLate`)。
let cancel_requested = cancel.load(Ordering::SeqCst);
@@ -270,11 +280,9 @@ pub(super) fn target_of(
.clone()
.filter(|name| !name.trim().is_empty())
.unwrap_or_else(|| convert::pack_stem(&request.pack));
let target = env
.layout
.data()
.join("instances")
.join(convert::dir_name(&name));
// **M216 P3-2**:落点也委托 core(`pack_dir::instance_dir`)——此前这里自己拼
// `<data>/instances/<dir_name>`,core 改了目录语义时 GUI 不会跟着走。
let target = pack_dir::instance_dir(&env.layout, &name);
if target.exists() {
// 空目录(多半是上一次失败的残留)**可以复用**,否则重试永远报「已存在」
let mut entries = std::fs::read_dir(&target)
+17
View File
@@ -51,6 +51,23 @@ struct Job<'a> {
}
impl Job<'_> {
/// 把 core 的运行期告警与「清单被替换」写进安装日志(**M216 P2-2**)。
///
/// 面板只有一行、Toast 4 秒就消失,安装日志是用户事后能自己回看的唯一渠道:动了共享
/// `.minecraft` 里那份 `versions/<id>/<id>.json` 这件事必须先落在这里(issue #M216 复审
/// P2-2:`report.manifest` 以前零消费者,GUI 侧对用户完全静默)。
fn journal_report(&self, report: &InstallReport) {
for warning in &report.warnings {
self.journal.line(warning);
}
if let Some(backup) = report.manifest.as_ref().and_then(|write| write.backup()) {
self.journal.line(&format!(
"版本清单已替换,旧的那份备份在 {}",
backup.display()
));
}
}
/// processor 段的进度出口(同一套 channel/meter/journal)。
fn reporter(&self) -> install_processors::Reporter<'_> {
install_processors::Reporter {
@@ -86,6 +86,7 @@ pub(super) fn install_vanilla(
)
.await
.map_err(|error| job_error(job, error))?;
job.journal_report(&report);
ensure_complete(&report)?;
Ok::<String, String>(version.to_string())
})
@@ -178,6 +179,7 @@ pub(super) fn install_loader(
.await
})
.map_err(|error| job_error(job, error))?;
job.journal_report(&report);
ensure_complete(&report)?;
Ok(release.version_id)
}