Carry: leave out the files the content loader would refuse
Vanilla skips a resource with an invalid path, a data file that does not parse, one at a time; Horizon's content loader refuses the whole mod set. carry now keeps them in outside/ with the reason (report `unreadable`): upper-case paths, JSON that does not parse, shared tags with "replace": true. A byte-order mark is dropped. ATM10's 42 carried mods: 19 such files in 7 mods (Mekanism Tools' ry_UA lang, CTOV's empty tag, Towns and Towers' replacing tags, broken template pools, recipes and a lang file), and the 42 load as carried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ rust-version = "1.86"
|
||||
description = "Port a Java modpack to Horizon: import, inventory, classification and data carry-over (docs/modpacks.md)"
|
||||
|
||||
[dependencies]
|
||||
# Content path rules only (no runtime): what the game's content loader accepts.
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha1 = "0.10"
|
||||
@@ -14,8 +16,5 @@ toml = "1.1.6"
|
||||
ureq = { version = "2", default-features = false, features = ["tls"] }
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -19,6 +19,10 @@ pub struct Report {
|
||||
/// Files outside what a content mod may hold (other namespaces, vanilla
|
||||
/// data other than tags), kept in `outside/` for review.
|
||||
pub outside: Vec<String>,
|
||||
/// Files of the mod's own content the game would refuse, kept in
|
||||
/// `outside/` too, with why: a path vanilla does not read (upper case),
|
||||
/// JSON that does not parse, a vanilla tag the mod replaces.
|
||||
pub unreadable: BTreeMap<String, String>,
|
||||
pub recipes_upgraded: usize,
|
||||
pub loot_tables_upgraded: usize,
|
||||
/// Worldgen and structure files (`data/<id>/worldgen/`, `structure/`).
|
||||
@@ -291,6 +295,32 @@ fn place(name: &str, id: &str) -> Option<bool> {
|
||||
Some(ns == id || shared_tags)
|
||||
}
|
||||
|
||||
/// Why the game would refuse a file of the mod's own content, if it would.
|
||||
/// Vanilla skips such files one by one (an invalid resource path, a data
|
||||
/// file that does not parse); Horizon's content loader refuses the whole
|
||||
/// set, so they stay out of the carry.
|
||||
fn unreadable(name: &str, bytes: &mut Vec<u8>) -> Option<String> {
|
||||
if let Err(e) = horizon_mods::content::check_path(name) {
|
||||
return Some(e);
|
||||
}
|
||||
if !(name.ends_with(".json") || name.ends_with(".mcmeta")) {
|
||||
return None;
|
||||
}
|
||||
// A byte-order mark: Horizon's JSON readers take the file without it.
|
||||
if bytes.starts_with(b"\xEF\xBB\xBF") {
|
||||
bytes.drain(..3);
|
||||
}
|
||||
let value: Value = match serde_json::from_slice(bytes) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Some(format!("does not parse as JSON: {e}")),
|
||||
};
|
||||
let shared_tag = name.starts_with("data/minecraft/tags/") || name.starts_with("data/c/tags/");
|
||||
if shared_tag && value.get("replace").and_then(Value::as_bool) == Some(true) {
|
||||
return Some("replaces a shared tag; a content mod may only add to it".into());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Carries `jar` over into `out/carry/`. With the oracle's `facts`, blocks
|
||||
/// and items are the ones the mod registers, with the templates and numbers
|
||||
/// the facts give; `allowed` says which vanilla blocks the game has.
|
||||
@@ -334,6 +364,15 @@ pub fn carry(
|
||||
entry
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|e| format!("{name}: {e}"))?;
|
||||
let unreadable = if keep {
|
||||
unreadable(&name, &mut bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let keep = keep && unreadable.is_none();
|
||||
if let Some(why) = unreadable {
|
||||
report.unreadable.insert(name.clone(), why);
|
||||
}
|
||||
let dest = if keep {
|
||||
report.copied += 1;
|
||||
inside.join(&name)
|
||||
@@ -716,6 +755,17 @@ mod tests {
|
||||
r#"{"values": ["gem:gem_block"]}"#,
|
||||
),
|
||||
("data/c/tags/item/gems.json", r#"{"values": ["gem:gem"]}"#),
|
||||
// What the game would refuse: vanilla skips these files alone.
|
||||
("assets/gem/lang/ry_UA.json", r#"{"item.gem.gem": "Gem"}"#),
|
||||
("data/gem/tags/item/empty.json", ""),
|
||||
(
|
||||
"data/minecraft/tags/worldgen/biome/has_structure/village_plains.json",
|
||||
r#"{"replace": true, "values": []}"#,
|
||||
),
|
||||
(
|
||||
"assets/gem/lang/de_de.json",
|
||||
"\u{feff}{\"item.gem.gem\": \"Juwel\"}",
|
||||
),
|
||||
];
|
||||
for (name, text) in files {
|
||||
zip.start_file(*name, zip::write::SimpleFileOptions::default())
|
||||
@@ -732,8 +782,23 @@ mod tests {
|
||||
assert_eq!(info.census.classes, 1);
|
||||
let out = dir.join("work/mods/gem");
|
||||
let report = carry(&jar, &info, &out, None, &|_| true).unwrap();
|
||||
assert_eq!(report.copied, 12);
|
||||
assert!(report.outside.is_empty());
|
||||
assert_eq!(report.copied, 13);
|
||||
let unreadable: Vec<&str> = report.unreadable.keys().map(String::as_str).collect();
|
||||
assert_eq!(
|
||||
unreadable,
|
||||
[
|
||||
"assets/gem/lang/ry_UA.json",
|
||||
"data/gem/tags/item/empty.json",
|
||||
"data/minecraft/tags/worldgen/biome/has_structure/village_plains.json",
|
||||
]
|
||||
);
|
||||
assert_eq!(report.outside.len(), 3);
|
||||
assert!(!out.join("carry/data/gem/tags/item/empty.json").exists());
|
||||
// A byte-order mark is dropped, the file kept.
|
||||
assert_eq!(
|
||||
fs::read(out.join("carry/assets/gem/lang/de_de.json")).unwrap()[0],
|
||||
b'{'
|
||||
);
|
||||
assert_eq!((report.recipes_upgraded, report.recipes_kept.len()), (1, 1));
|
||||
assert_eq!(report.item_definitions_added, 2);
|
||||
assert_eq!(report.blocks["gem_door"], "minecraft:oak_door");
|
||||
|
||||
+6
-2
@@ -302,8 +302,12 @@ only): content 54, library 33, client-ui 26, performance 25, other 14, unknown 2
|
||||
|
||||
`carry` copies a jar's `assets/<id>/`, `data/<id>/`, `data/minecraft/tags/` and `data/c/tags/`
|
||||
into `mods/<id>/carry/`, a folder content mod (API 0.2) the game loads with `--dev-mod`, and
|
||||
keeps what a content mod may not hold (other namespaces, vanilla data) in `outside/`. No class
|
||||
file is copied. It upgrades from 1.21.1 to 26.3: vanilla-type recipes (ingredients), loot tables
|
||||
keeps what a content mod may not hold (other namespaces, vanilla data) in `outside/`. Files of
|
||||
the mod's own content that the game's content loader would refuse whole also go to `outside/`,
|
||||
listed with the reason in the report's `unreadable`: paths vanilla does not read (upper case),
|
||||
JSON that does not parse, and shared tags with `"replace": true`. Vanilla skips them one by one.
|
||||
A byte-order mark is dropped. For ATM10's 42 mods that is 19 files in 7 mods, and the 42 load as
|
||||
carried. No class file is copied. It upgrades from 1.21.1 to 26.3: vanilla-type recipes (ingredients), loot tables
|
||||
(`conditions` lists become one `condition`, `functions` become `modifier`, kinds move to `type`,
|
||||
tool matches on silk touch or shears become `minecraft:tool/can_silk_touch` and
|
||||
`minecraft:tool/can_shear`), and the `items/` definitions 1.21.4 introduced.
|
||||
|
||||
Reference in New Issue
Block a user