fix(core): M110 r2 —— duplicate_override_paths 两种冲突同时报,补 mrpack 同侧分支用例
- p2-2:`duplicate_override_paths` 改成「先按 path 分组,再在组内报 (侧集合, 条目份数)」, 判据 `侧数 > 1 || 份数 > 侧数`。r1 修好了跨侧(Both×Client)但按侧去重把「同侧两个组 撞同一路径」这条旧能力换掉了(实测返回 []);现在两种冲突同时报得出来,返回值升为 `Vec<(String, Vec<PackSide>, usize)>`(M111 注意:这是最后一次改形状的窗口)。 - p3-a:mrpack `check_overrides` 的「每侧只能有一个覆盖目录」分支补用例(此前零测试, 删掉它 54 条全绿;现在删掉即红)。 - p3-b:把 ReadBudget 的「每次调用一份额度,不是整包一份」写进 `override_files` / `ReadBudget` 文档(mrpack 连调三次 ⇒ 理论上限 3×;要整包共用额度就走 `override_files_with`;`open` 阶段的声明总量仍是整包一次)。 - 顺带(复审点名、各一行):mrpack 非表达目录的 hint 里 21 个连续空格; curseforge 文档里 `.minecraft/.minecraft/…` 的说法按 CF 语义订正(overrides 是 zip 内前缀, 内容铺开不丢;要不要多拷一层取决于导入器)。
This commit is contained in:
1 parent
6dd652aced
commit
34f3db359a
7 files changed
+111
-22
No files matched your search
@@ -254,8 +254,13 @@ impl PackArchive {
|
||||
/// 目录不存在时返回空表——"包没带 overrides"不是错误。
|
||||
///
|
||||
/// 这是**唯一会批量解压的 API**,所以它自己持有一个累计预算
|
||||
/// (`PackLimits::max_uncompressed_bytes`),逐条按实际读出的字节扣减:
|
||||
/// 声明大小撒谎的包不会在这里把 CPU 时间耗光(r1 p3-4)。
|
||||
/// (`PackLimits::max_uncompressed_bytes`),本次调用内逐条按实际读出的字节扣减:
|
||||
/// 声明大小撒谎的包不会靠"很多条各自合规"把 CPU 时间耗光(r1 p3-4)。
|
||||
///
|
||||
/// **额度是"每次调用一份",不是"整包一份"**(r2 p3-b):格式读者若对多个覆盖目录各调一次
|
||||
/// (mrpack 会连调三次:`overrides` / `client-overrides` / `server-overrides`),理论上限
|
||||
/// 是 `max_uncompressed_bytes × 调用次数`。要整包共用一份额度,就自己建 [`ReadBudget`]
|
||||
/// 并走 [`Self::override_files_with`];`open` 阶段的"声明总量"仍然是整包只判一次。
|
||||
pub fn override_files(&self, dir: &str) -> PackResult<Vec<OverrideFile>> {
|
||||
let budget = ReadBudget::new(self.limits.max_uncompressed_bytes);
|
||||
self.override_files_with(dir, &budget)
|
||||
|
||||
@@ -59,6 +59,10 @@ impl Default for PackLimits {
|
||||
|
||||
/// 一次批量读取的**累计**预算(读取阶段,按实际读出的字节扣减)。
|
||||
///
|
||||
/// 作用域是"拿到它的那次扫描":`PackArchive::override_files` 每次调用新建一份,
|
||||
/// 所以要跨多次调用共享额度(例如整包只给一份 `max_uncompressed_bytes`),
|
||||
/// 必须由调用方自建一个并交给 `override_files_with`(r2 p3-b)。
|
||||
///
|
||||
/// 为什么需要它:中央目录声明的大小可以撒谎,单个条目也被限制在
|
||||
/// [`PackLimits::max_entry_bytes`],但"很多条各自合规"累起来仍可能远超
|
||||
/// `max_uncompressed_bytes`。批量读取(`override_files`)持有一个预算,边读边扣。
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
//! `disabled` 即 `required: false`。`client`/`server` 分侧语义 CF 表达不了(mrpack 专属)。
|
||||
//! - **跨格式导出不完整**(M112 的活):`modLoaders[].id` 只对 CF 来源是合法形状;
|
||||
//! MultiMC 的 `net.fabricmc.fabric-loader` / 非加载器组件会被逐字写进去(CF 不认);
|
||||
//! `overrides` 字段会照抄模型里的目录名(从 MultiMC 转过来是 `.minecraft`,落到 ULL 实例里
|
||||
//! 会变成 `.minecraft/.minecraft/…`)。同一份模型只有 `read` 出来的那个格式才保证往返。
|
||||
//! `overrides` 字段会照抄模型里的目录名(从 MultiMC 转过来是 `.minecraft`)。
|
||||
//! 按 CF 语义 `overrides` 只是 zip 内**前缀**,导入器把该前缀下的内容铺到游戏目录,
|
||||
//! 所以内容不会丢;但"前缀目录本身要不要跟着拷一层"取决于导入器实现(Prism/ULL 的实例根
|
||||
//! 本身就叫 `.minecraft` 时可能多出一层)。同一份模型只有 `read` 出来的那个格式才保证往返。
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
@@ -333,28 +333,42 @@ impl PackManifest {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 跨覆盖组撞同一实例路径的清单:`(路径, 撞车的侧集合)`,按路径排序。
|
||||
/// 撞同一实例路径的覆盖条目:`(路径, 涉及的侧, 条目份数)`,按路径排序。
|
||||
///
|
||||
/// **按 `path` 跨侧计数**,不是按 `(side, path)`:mrpack 里 `overrides/`(Both 侧)与
|
||||
/// `client-overrides/`(Client 侧)写到同一个实例路径才是真实冲突,而两侧的键天然不同
|
||||
/// (r1 p2-2 之前这里恒返回空表,正好漏掉它要支撑的那个决策)。
|
||||
/// **两种冲突都要报**(r2 p2-2:上一版按侧去重,把"同侧撞车"这条能力换掉了):
|
||||
///
|
||||
/// 返回的多侧冲突由导入器决定优先级(本模块只报告,不选边)。
|
||||
/// - **跨侧**:`overrides/`(Both)与 `client-overrides/`(Client)写同一个实例路径——
|
||||
/// mrpack 唯一的真实冲突。按 `(side, path)` 计数会恒返回空表(两侧键天然不同)。
|
||||
/// - **同侧**:两个同侧组撞同一路径,或一个组里同一路径列了两次(模型被合并时才出现)。
|
||||
/// 按侧去重会把这份重复折叠掉,于是同样"查不出来"。
|
||||
///
|
||||
/// 判据:`侧数 > 1 || 条份数 > 侧数`(后者即同侧重复)。冲突由导入器决定优先级,
|
||||
/// 本模块只报告、不选边。
|
||||
#[must_use]
|
||||
pub fn duplicate_override_paths(&self) -> Vec<(String, Vec<PackSide>)> {
|
||||
let mut sides: HashMap<&str, Vec<PackSide>> = HashMap::new();
|
||||
pub fn duplicate_override_paths(&self) -> Vec<(String, Vec<PackSide>, usize)> {
|
||||
struct Slot {
|
||||
sides: Vec<PackSide>,
|
||||
occurrences: usize,
|
||||
}
|
||||
|
||||
let mut slots: HashMap<&str, Slot> = HashMap::new();
|
||||
for (side, file) in self.override_files() {
|
||||
let entry = sides.entry(file.path.as_str()).or_default();
|
||||
if !entry.contains(&side) {
|
||||
entry.push(side);
|
||||
let slot = slots.entry(file.path.as_str()).or_insert(Slot {
|
||||
sides: Vec::new(),
|
||||
occurrences: 0,
|
||||
});
|
||||
slot.occurrences += 1;
|
||||
if !slot.sides.contains(&side) {
|
||||
slot.sides.push(side);
|
||||
}
|
||||
}
|
||||
let mut duplicated: Vec<(String, Vec<PackSide>)> = sides
|
||||
|
||||
let mut duplicated: Vec<(String, Vec<PackSide>, usize)> = slots
|
||||
.into_iter()
|
||||
.filter(|(_, sides)| sides.len() > 1)
|
||||
.map(|(path, mut sides)| {
|
||||
sides.sort_by_key(|side| side.as_str());
|
||||
(path.to_string(), sides)
|
||||
.filter(|(_, slot)| slot.sides.len() > 1 || slot.occurrences > slot.sides.len())
|
||||
.map(|(path, mut slot)| {
|
||||
slot.sides.sort_by_key(|side| side.as_str());
|
||||
(path.to_string(), slot.sides, slot.occurrences)
|
||||
})
|
||||
.collect();
|
||||
duplicated.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
|
||||
@@ -154,7 +154,8 @@ impl PackFormat for Modrinth {
|
||||
group.dir
|
||||
))
|
||||
.with_hint(
|
||||
"按 side 分派到 overrides / client-overrides / server-overrides 属 M112; 不同格式的实例目录(如 MultiMC 的 .minecraft)不能直接搬过来",
|
||||
"按 side 分派到 overrides / client-overrides / server-overrides 属 M112;\
|
||||
不同格式的实例目录(如 MultiMC 的 .minecraft)不能直接搬过来",
|
||||
));
|
||||
}
|
||||
if seen.contains(&group.side) {
|
||||
|
||||
@@ -127,3 +127,30 @@ fn same_format_export_is_still_allowed() {
|
||||
.check_overrides(&manifest)
|
||||
.expect("mrpack 自己读出来的模型必须能被 mrpack 表达");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mrpack_refuses_two_override_groups_for_the_same_side() {
|
||||
// r2 p3-a:mrpack 的「每侧只能有一个覆盖目录」分支此前零测试(删掉它 59 条全绿)。
|
||||
// 目录名检查管不到这里:两个组的 `dir` 都合法,区别只在于同一侧出现了两次。
|
||||
let mut manifest = PackManifest::new("同侧两个覆盖目录");
|
||||
manifest.version = Some("1.0.0".to_string());
|
||||
manifest.minecraft = Some("1.20.1".to_string());
|
||||
manifest
|
||||
.overrides
|
||||
.push(PackOverrides::new("overrides", PackSide::Both));
|
||||
manifest
|
||||
.overrides
|
||||
.push(PackOverrides::new("overrides", PackSide::Both));
|
||||
|
||||
let target = temp_root("export-mr-same-side").join("out.mrpack");
|
||||
let error = MODRINTH
|
||||
.write(&manifest, PackPayload::metadata_only(), &target)
|
||||
.expect_err("同一侧两个覆盖目录必须拒");
|
||||
assert_eq!(error.kind(), PackErrorKind::Unsupported, "{error}");
|
||||
assert!(
|
||||
error.message().contains("每个侧只能有一个覆盖目录"),
|
||||
"错误要指出是「同侧重复」而不是「目录名不对」:{}",
|
||||
error.message()
|
||||
);
|
||||
assert!(!target.exists(), "被拒时不许留下目标文件");
|
||||
}
|
||||
@@ -33,9 +33,45 @@ fn duplicate_override_paths_counts_across_sides_not_per_side() {
|
||||
manifest.duplicate_override_paths(),
|
||||
vec![(
|
||||
"config/x.toml".to_string(),
|
||||
vec![PackSide::Both, PackSide::Client]
|
||||
vec![PackSide::Both, PackSide::Client],
|
||||
2
|
||||
)],
|
||||
"只报真正撞车的路径,并给出涉及的侧"
|
||||
"只报真正撞车的路径,并给出涉及的侧与条目份数"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_override_paths_still_reports_two_groups_on_the_same_side() {
|
||||
// r2 p2-2:上一版按侧去重,把这半能力换掉了 —— 两个 Both 组撞同一路径实测返回 `[]`。
|
||||
// 现在判据是「侧数 > 1 || 条份数 > 侧数」,两种冲突同时报得出来。
|
||||
let mut first = PackOverrides::new("overrides", PackSide::Both);
|
||||
first.files.push(file("config/x.toml", "a", 1));
|
||||
let mut second = PackOverrides::new("extra-overrides", PackSide::Both);
|
||||
second.files.push(file("config/x.toml", "b", 2));
|
||||
let manifest = PackManifest {
|
||||
overrides: vec![first, second],
|
||||
..PackManifest::default()
|
||||
};
|
||||
assert_eq!(
|
||||
manifest.duplicate_override_paths(),
|
||||
vec![("config/x.toml".to_string(), vec![PackSide::Both], 2)],
|
||||
"同侧两个组撞同一路径必须报(侧集合只有一个,份数是 2)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_override_paths_reports_the_same_path_twice_inside_one_group() {
|
||||
// 同侧撞车的另一种形态:一个组里同一路径列了两次(模型被合并出来时会出现)。
|
||||
let mut group = PackOverrides::new("overrides", PackSide::Both);
|
||||
group.files.push(file("config/x.toml", "a", 1));
|
||||
group.files.push(file("config/x.toml", "b", 2));
|
||||
let manifest = PackManifest {
|
||||
overrides: vec![group],
|
||||
..PackManifest::default()
|
||||
};
|
||||
assert_eq!(
|
||||
manifest.duplicate_override_paths(),
|
||||
vec![("config/x.toml".to_string(), vec![PackSide::Both], 2)]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user