Compare commits

...
Author SHA1 Message Date
Ludovic Marie de840da0e5 Merge pull request #1594 from Domanffe/feat/bundled-languages
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 00:15:26 +02:00
Ludovic MarieandClaude Opus 5.5 c6980fb2b7 Drop the lang.rs test: layers_selected_language_over_english covers it
It reads the same files the same way (assets/pack.mcmeta's language
section, a language offered only with its file, Language::load over
en_us). Leaving the client out also keeps this PR to horizon-prepare, so
its check doesn't rerun the client's harnesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:43:26 +02:00
Alexander KrylovandHermes Agent adcfe810f2 rustfmt
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 22:46:35 +03:00
Alexander KrylovandHermes Agent 3802a97dc9 Language: the prepared resources carry every bundled language and the list
The language screen offered English alone. The client jar holds only
`en_us.json` and `deprecated.json` under `assets/minecraft/lang`, and the list
the screen shows comes from `pack.mcmeta`'s `language` section, which the jar
does not carry at all. Both live in the asset index, so `horizon-prepare
languages` copies `pack.mcmeta` and every `minecraft/lang/*.json` from the
asset store the way the sounds, the panorama and Unifont already are, and full
preparation runs it with the rest.

Every bundle the client declares is then offered, `Русский` among them, and
`lang` in options.txt selects one (#1272).

Fixes #1272.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 22:42:39 +03:00
3 changed files with 80 additions and 6 deletions

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@ pub mod pyjson;
mod sounds;
mod survival;
pub use sounds::{fonts, packs, panorama, sounds, BUILT_IN_PACKS};
pub use sounds::{fonts, languages, packs, panorama, sounds, BUILT_IN_PACKS};
use pyjson::Value;
use sha2::Digest;
+12 -3
View File
@@ -20,6 +20,7 @@ Usage: horizon-prepare [--prepare] [--server-jar PATH] [--minecraft-dir PATH] [-
horizon-prepare panorama [--minecraft-dir PATH] [--root PATH]
horizon-prepare fonts [--minecraft-dir PATH] [--root PATH]
horizon-prepare packs [--minecraft-dir PATH] [--root PATH]
horizon-prepare languages [--minecraft-dir PATH] [--root PATH]
--prepare Import the pinned game files from your own copy first
--server-jar PATH The pinned server jar you downloaded yourself
@@ -36,7 +37,8 @@ The resources go to <root>/.local/horizon-client/<version>; pass that to horizon
horizon-prepare panorama repairs the classic menu background in existing resources without Java.
horizon-prepare fonts adds the GNU Unifont glyphs (characters the bitmap fonts lack) the same way.
horizon-prepare packs adds the built-in Programmer Art and High Contrast resource packs the same way.
Full preparation also imports all three after extracting the client JAR's placeholders.
horizon-prepare languages adds the bundled languages and the language list the JAR lacks the same way.
Full preparation also imports all four after extracting the client JAR's placeholders.
horizon-prepare sounds copies the pinned version's sounds (about 380 MB) from the official
launcher's asset store into the same folder, checking every file. It is optional: without it the
@@ -68,8 +70,12 @@ fn main() -> ExitCode {
};
let mut root = None;
let mut args = std::env::args_os().skip(1).peekable();
let assets =
args.next_if(|a| matches!(a.to_str(), Some("sounds" | "panorama" | "fonts" | "packs")));
let assets = args.next_if(|a| {
matches!(
a.to_str(),
Some("sounds" | "panorama" | "fonts" | "packs" | "languages")
)
});
while let Some(arg) = args.next() {
let text = arg.to_string_lossy().into_owned();
let (flag, inline) = match text.split_once('=') {
@@ -138,6 +144,9 @@ fn main() -> ExitCode {
Some("packs") => {
horizon_prepare::packs(&options.root, options.minecraft_dir.as_deref(), &log)
}
Some("languages") => {
horizon_prepare::languages(&options.root, options.minecraft_dir.as_deref(), &log)
}
_ => horizon_prepare::prepare(&options, &log),
};
match result {
+67 -2
View File
@@ -112,6 +112,20 @@ pub fn fonts(root: &Path, minecraft: Option<&Path>, log: Log) -> Result<PathBuf,
copy_assets(root, minecraft, log, is_font, "Font", &UNIFONT)
}
/// Every bundled language and the language list, both of which only the asset index holds
/// (the client JAR carries `en_us` alone): `pack.mcmeta`'s `language` section is what the
/// language screen offers, `minecraft/lang/<code>.json` the strings each one draws (#1272).
/// Also usable on already-prepared resources without Java.
pub fn languages(root: &Path, minecraft: Option<&Path>, log: Log) -> Result<PathBuf, String> {
copy_assets(root, minecraft, log, is_language, "Language", &LANGUAGES)
}
const LANGUAGES: [&str; 1] = ["pack.mcmeta"];
fn is_language(name: &str) -> bool {
name == "pack.mcmeta" || name.starts_with("minecraft/lang/")
}
/// Full preparation can run from cached JARs without an official launcher install.
pub(crate) fn optional_assets(root: &Path, minecraft: Option<&Path>, log: Log) {
if let Err(error) = panorama(root, minecraft, log) {
@@ -129,6 +143,11 @@ pub(crate) fn optional_assets(root: &Path, minecraft: Option<&Path>, log: Log) {
"Programmer Art and High Contrast not imported: {error}; run horizon-prepare packs later"
));
}
if let Err(error) = languages(root, minecraft, log) {
log(&format!(
"Languages not imported: {error}; run horizon-prepare languages later"
));
}
}
const PANORAMA: [&str; 7] = [
@@ -278,6 +297,15 @@ mod tests {
}
fn with_panorama(name: &str, panorama: &[&'static str]) -> Fixture {
Self::with_extras(name, panorama, &[])
}
/// The same store with extra index entries, for the files the index alone holds.
fn with_extras(
name: &str,
panorama: &[&'static str],
extras: &[(&'static str, &'static [u8])],
) -> Fixture {
let dir = std::env::temp_dir().join(format!(
"horizon-prepare-sounds-{name}-{}",
std::process::id()
@@ -302,6 +330,9 @@ mod tests {
// Distinct object bytes let the test catch swapped faces too.
objects.push((name, name.as_bytes().to_vec()));
}
for &(name, data) in extras {
objects.push((name, data.to_vec()));
}
let mut index = String::from("{\"objects\": {");
for (i, (name, data)) in objects.iter().enumerate() {
let hash = sha1_hex(data);
@@ -391,6 +422,32 @@ mod tests {
f.run().unwrap();
}
#[test]
fn languages_come_from_the_asset_store() {
// The client JAR carries `en_us` alone, so every other language and the list in
// `pack.mcmeta` come from the index (#1272).
let f = Fixture::with_extras(
"languages",
&[],
&[("pack.mcmeta", b"{\"language\": {\"fr_fr\": {}}}")],
);
let lines = std::cell::RefCell::new(Vec::new());
let target = languages(&f.root, Some(&f.minecraft), &|l| {
lines.borrow_mut().push(l.to_string())
})
.unwrap();
assert!(fs::read(target.join("assets/pack.mcmeta"))
.unwrap()
.starts_with(b"{\"language\""));
assert_eq!(
fs::read(target.join("assets/minecraft/lang/fr_fr.json")).unwrap(),
b"{}"
);
// Each entry point copies its own set: the sounds stay where they belong.
assert!(!target.join("assets/minecraft/sounds.json").exists());
assert_eq!(*lines.borrow(), ["Language assets ready: 2 files, 0.0 MB"]);
}
/// #1076: the launcher holds an index Mojang republished under the same id, so its SHA-1
/// is not the one in the version JSON. A known one is used quietly, an unknown one with a
/// line in the log; the sounds come through either way.
@@ -539,7 +596,10 @@ mod tests {
lines[1..],
[
"Font assets ready: 2 files, 0.0 MB",
"Built-in pack assets ready: 2 files, 0.0 MB"
"Built-in pack assets ready: 2 files, 0.0 MB",
// This store has no `pack.mcmeta`, so the languages report it.
"Languages not imported: pack.mcmeta is missing from the asset index; \
run horizon-prepare languages later"
]
),
_ => {
@@ -549,9 +609,14 @@ mod tests {
&& lines[2].ends_with("; run horizon-prepare packs later"),
"{lines:?}"
);
assert!(
lines[3].starts_with("Languages not imported: ")
&& lines[3].ends_with("; run horizon-prepare languages later"),
"{lines:?}"
);
}
}
assert_eq!(lines.len(), 3);
assert_eq!(lines.len(), 4);
for name in PANORAMA {
assert_eq!(fs::read(target.join(name)).unwrap(), b"jar placeholder");
}