mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Prepare with horizon-prepare: launcher, release folders, messages and docs
The launcher prepares a version in-process when it ships the preparer sources the launcher was built with, with the version's own horizon-prepare when they differ, and with the Python script only for versions made before it. Release folders ship horizon-prepare and its sources; resources are reused between versions with the same preparer. The game's messages, PLAY.txt, the docs and release builds name horizon-prepare, and tools/prepare_native_client.py is a wrapper of it for the development tools. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9fa3a684c6
commit
a995fd3b46
22 files changed
+275
-521
No files matched your search
@@ -1,6 +1,6 @@
|
||||
name: release builds
|
||||
# Started by hand: gh workflow run release.yml --ref <branch>
|
||||
# Builds the Windows x64 and Linux x86_64 clients (with horizon-ctl) and uploads the playable folders
|
||||
# Builds the Windows x64 and Linux x86_64 clients (with horizon-ctl and horizon-prepare) and uploads the playable folders
|
||||
# as artifacts.
|
||||
# macOS builds are made on the development machines. Linux is built here, on an older Ubuntu,
|
||||
# so the binary uses the standard loader and runs on any recent distribution (and on NixOS
|
||||
@@ -20,13 +20,14 @@ jobs:
|
||||
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
|
||||
env:
|
||||
RUSTFLAGS: -C target-feature=+crt-static
|
||||
run: cargo +stable build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl
|
||||
run: cargo +stable build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare
|
||||
- name: Only system DLLs
|
||||
# Vulkan and the rest are loaded at run time; a linked non-system DLL is a
|
||||
# "DLL not found" dialog on some players' machines.
|
||||
run: |
|
||||
python tools/check_imports.py target/release/horizon-client.exe
|
||||
python tools/check_imports.py target/release/horizon-ctl.exe
|
||||
python tools/check_imports.py target/release/horizon-prepare.exe
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: packaging/package.sh target/release/horizon-client.exe dist/horizon-windows-x64
|
||||
@@ -43,11 +44,12 @@ jobs:
|
||||
- name: ALSA headers
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev
|
||||
- name: Build the client
|
||||
run: cargo +stable build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl
|
||||
run: cargo +stable build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare
|
||||
- name: Standard loader and libraries only
|
||||
run: |
|
||||
python3 tools/check_imports.py target/release/horizon-client
|
||||
python3 tools/check_imports.py target/release/horizon-ctl
|
||||
python3 tools/check_imports.py target/release/horizon-prepare
|
||||
- name: Package
|
||||
run: packaging/package.sh target/release/horizon-client dist/horizon-linux-x86_64
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -140,7 +140,7 @@ Horizon contains no game files and never downloads them. It builds its resources
|
||||
```sh
|
||||
git clone https://github.com/ludovic111/horizon.git
|
||||
cd horizon
|
||||
python3 tools/prepare_native_client.py --prepare --server-jar ~/Downloads/server.jar # needs JDK 25
|
||||
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar # needs JDK 25
|
||||
python3 tools/fetch_sounds.py # optional: sounds from your launcher install
|
||||
cargo run --release -p horizon-client
|
||||
```
|
||||
|
||||
@@ -243,7 +243,7 @@ impl Options {
|
||||
match arg.as_str() {
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"Horizon — Minecraft Java Edition 26.3 in Rust\n\n --resources PATH Prepared resources (tools/prepare_native_client.py)\n --render-distance N Chunks, 2..64 (default 12)\n --world NAME World folder under saves/ (default \"New World\")\n --seed N World seed for new worlds (default 0)\n --creative Creative mode (default survival)\n --test-terrain Simple test terrain instead of vanilla generation\n --frames N Exit after N frames (testing)\n --smoke-report PATH Write a JSON report on exit\n --control Accept horizon-ctl commands on a local socket\n --control-socket PATH Control socket path (default $XDG_RUNTIME_DIR/horizon-control.sock)\n --server HOST[:PORT] Join a multiplayer server (offline mode) at start\n --name NAME Player name for multiplayer (default Player)\n\nControls: mouse look, WASD move, Space jump, Left Shift sneak, Left Ctrl sprint,\nleft click break, right click place/use, 1-9/scroll hotbar, E inventory,\nQ drop, F3 debug, Esc pause."
|
||||
"Horizon — Minecraft Java Edition 26.3 in Rust\n\n --resources PATH Prepared resources (horizon-prepare)\n --render-distance N Chunks, 2..64 (default 12)\n --world NAME World folder under saves/ (default \"New World\")\n --seed N World seed for new worlds (default 0)\n --creative Creative mode (default survival)\n --test-terrain Simple test terrain instead of vanilla generation\n --frames N Exit after N frames (testing)\n --smoke-report PATH Write a JSON report on exit\n --control Accept horizon-ctl commands on a local socket\n --control-socket PATH Control socket path (default $XDG_RUNTIME_DIR/horizon-control.sock)\n --server HOST[:PORT] Join a multiplayer server (offline mode) at start\n --name NAME Player name for multiplayer (default Player)\n\nControls: mouse look, WASD move, Space jump, Left Shift sneak, Left Ctrl sprint,\nleft click break, right click place/use, 1-9/scroll hotbar, E inventory,\nQ drop, F3 debug, Esc pause."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -5617,7 +5617,7 @@ impl Game<'_> {
|
||||
)
|
||||
} else {
|
||||
let inner = Arc::new(VanillaTerrain::for_dimension(&self.options.resources.join("worldgen"), res.assets.registry, &res.biomes, seed, dim.id()).map_err(|e| {
|
||||
format!("{e}\nPrepare the worldgen bundle with: python3 tools/prepare_native_client.py")
|
||||
format!("{e}\nPrepare the worldgen bundle with the launcher, or: horizon-prepare --prepare")
|
||||
})?);
|
||||
self.end.set_locator(Some(inner.clone()));
|
||||
Streaming::new(
|
||||
@@ -8692,7 +8692,7 @@ pub fn run() -> Result<()> {
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"{e}\nPrepare resources with: python3 tools/prepare_native_client.py --prepare"
|
||||
"{e}\nPrepare resources with the launcher, or: horizon-prepare --prepare"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ impl Game<'_> {
|
||||
let root = self.options.resources.clone();
|
||||
let builtin = Builtin::find(&root)
|
||||
.and_then(|p| Builtin::load(&p).ok())
|
||||
.ok_or("registries.json not found next to the resources; run the data generator reports (tools/prepare_native_client.py)")?;
|
||||
.ok_or("registries.json not found next to the resources; run the data generator reports (horizon-prepare)")?;
|
||||
let lang = Arc::new(horizon_client::net::load_language(&root));
|
||||
let view = self.settings.render_distance.clamp(2, 32) as i8;
|
||||
let session = Session::connect(address, name, view, lang.clone());
|
||||
|
||||
@@ -62,7 +62,7 @@ impl VanillaTerrain {
|
||||
if !dir.join("blocks/block-properties.nbt").exists() {
|
||||
eprintln!(
|
||||
"Horizon: the worldgen bundle lacks the block behavior exports (worldgen/blocks/*.nbt); \
|
||||
run python3 tools/prepare_native_client.py to add them"
|
||||
prepare again (the launcher, or horizon-prepare --prepare) to add them"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! [`Behaviours`] holds immutable tables loaded once from the pinned client
|
||||
//! resources (`block-behaviours.json`, `gameplay-facts.json`,
|
||||
//! `block-tags.json`, `cached-collision.nbt`; see
|
||||
//! `tools/prepare_native_client.py`).
|
||||
//! `crates/horizon-prepare`).
|
||||
//!
|
||||
//! # Driving a level
|
||||
//!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Optional block-behavior registries used by feature placement: physical
|
||||
//! properties, face support, collision and survival rules. Loaded from the
|
||||
//! bundle's `blocks/` directory (Java-exported, see prepare_native_client.py).
|
||||
//! bundle's `blocks/` directory (Java-exported, see crates/horizon-prepare).
|
||||
use super::generator::GeneratorError;
|
||||
use crate::{
|
||||
block_collision::BlockCollisionRegistry,
|
||||
|
||||
@@ -14,7 +14,8 @@ missing; [Rust rewrite status](rust-parity.md) has the details.
|
||||
|
||||
## Run from a checkout
|
||||
|
||||
Use current stable Rust (1.90+), Python 3.10+ and JDK 25. Horizon never
|
||||
Use current stable Rust (1.90+) and JDK 25 (Python 3.10+ for the sounds and the
|
||||
development harnesses). Horizon never
|
||||
downloads the game: resource preparation imports **your own copy** (the 26.3
|
||||
client from your official launcher install, and the 26.3 server jar you
|
||||
downloaded yourself; see [Build from source](../README.md#build-from-source)),
|
||||
@@ -23,8 +24,12 @@ exporters against those jars, and extracts the jar's assets and data into ignore
|
||||
`.local/horizon-client/26.3/`. Sounds (about 380 MB) are copied from your
|
||||
launcher's asset store. Prepared resources are private; never commit them.
|
||||
|
||||
Preparation is `horizon-prepare` (`crates/horizon-prepare`, Rust): it needs no Python, only
|
||||
the JDK, for the small exporters that call Mojang's own code. Release folders ship it beside
|
||||
`horizon-client`, and the launcher prepares with it.
|
||||
|
||||
```sh
|
||||
python3 tools/prepare_native_client.py --prepare --server-jar ~/Downloads/server.jar
|
||||
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
|
||||
python3 tools/fetch_sounds.py
|
||||
cargo run --release -p horizon-client -- --render-distance 12
|
||||
```
|
||||
@@ -205,7 +210,7 @@ attribute. Sneaking invokes ledge backoff before collision resolution. The
|
||||
complete authoritative player tick remains unfinished.
|
||||
|
||||
```sh
|
||||
python3 tools/prepare_native_client.py
|
||||
cargo run --release -p horizon-prepare
|
||||
python3 tools/test_native_scene_collision.py
|
||||
```
|
||||
|
||||
@@ -264,7 +269,7 @@ The [renderer startup check](../benchmarks/rust-native-walking-2026-10-07/startu
|
||||
also passes after the movement integration.
|
||||
|
||||
```sh
|
||||
python3 tools/prepare_native_client.py
|
||||
cargo run --release -p horizon-prepare
|
||||
python3 tools/test_native_scene_motion.py
|
||||
python3 tools/test_local_input_parity.py
|
||||
```
|
||||
|
||||
@@ -82,7 +82,7 @@ python3 tools/test_section_mesh_parity.py --only fluids
|
||||
```
|
||||
|
||||
It needs the pinned jars (`tools/lab.py prepare`) and prepared client resources
|
||||
(`tools/prepare_native_client.py --prepare`). Outputs, logs and
|
||||
(`cargo run --release -p horizon-prepare -- --prepare`). Outputs, logs and
|
||||
`report.json` go to `target/section-mesh-parity/`; nothing is written under
|
||||
`.local/`. The fixture is built with `--profile fast` and reports its
|
||||
meshing time.
|
||||
|
||||
Generated
+22
@@ -2571,6 +2571,7 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"flate2",
|
||||
"gpui",
|
||||
"horizon-prepare",
|
||||
"horizon-theme",
|
||||
"image",
|
||||
"pulldown-cmark",
|
||||
@@ -2582,6 +2583,16 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-prepare"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"sha1",
|
||||
"sha2 0.10.9",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-theme"
|
||||
version = "0.1.0"
|
||||
@@ -4964,6 +4975,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -21,6 +21,7 @@ self-replace = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
horizon-theme = { path = "../crates/horizon-theme" }
|
||||
horizon-prepare = { path = "../crates/horizon-prepare" }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
+5
-2
@@ -15,8 +15,11 @@ The opening, then Play ([clip](docs/opening.webm)):
|
||||
- **Versions**: lists the published releases of ludovic111/horizon, installs the build for
|
||||
this platform into its own folder, and removes it again.
|
||||
- **Play**: prepares resources as `PLAY.txt` describes (your launcher's 26.3 install, the
|
||||
26.3 server jar from that version's own json, Python 3.10+ and Java 25), then starts the
|
||||
client, or a server (`--dedicated --nogui`) in a folder you choose, with a live console.
|
||||
26.3 server jar from that version's own json, and Java 25), then starts the client, or a
|
||||
server (`--dedicated --nogui`) in a folder you choose, with a live console. Preparing is
|
||||
horizon-prepare: linked into the launcher, used in-process when a version ships the same
|
||||
preparer sources, else that version's own `horizon-prepare`; versions released before it
|
||||
still prepare with their Python script and need Python 3.10+.
|
||||
- **Patch notes**: each release's notes, newest first, with a link to GitHub Sponsors. Quiet "Join Discord" and "Website" links sit in the sidebar footer.
|
||||
|
||||
Data lives in the platform's data directory (`~/.local/share/horizon-launcher` on Linux,
|
||||
|
||||
+144
-27
@@ -380,12 +380,15 @@ pub fn is_prepared(tag: &str) -> bool {
|
||||
|
||||
/// Whether the resources `from` prepared are the ones preparing `to` would make. The output
|
||||
/// depends on the Horizon version, not only on Minecraft's: the manifest records the hashes of
|
||||
/// the vanilla exporters it was made with (mods/horizon/src/test/java/.../parity), and
|
||||
/// prepare_native_client.py writes every file and the manifest's `schema`. So both need the
|
||||
/// same pinned Minecraft jars (research-lock.json), the same prepare_native_client.py and the
|
||||
/// same exporter sources. The helpers it imports (lab.py and the test_*_parity.py modules)
|
||||
/// change between releases in how they find and check the player's files, which the output
|
||||
/// does not depend on; they are not compared.
|
||||
/// the vanilla exporters it was made with (mods/horizon/src/test/java/.../parity), and the
|
||||
/// preparer writes every file and the manifest's `schema`. So both need the same pinned
|
||||
/// Minecraft jars (research-lock.json), the same preparer and the same exporter sources. The
|
||||
/// preparer is crates/horizon-prepare (its sources ship in the release folder beside the
|
||||
/// binary) and tools/prepare_native_client.py (the Python preparer of older versions, a wrapper
|
||||
/// of the binary since); a file only one of the two versions has is a different preparer. The
|
||||
/// helpers the Python preparer imported (lab.py and the test_*_parity.py modules) change
|
||||
/// between releases in how they find and check the player's files, which the output does not
|
||||
/// depend on; they are not compared.
|
||||
fn resources_compatible(from: &Path, to: &Path, manifest: &serde_json::Value) -> bool {
|
||||
let pins = |dir: &Path| {
|
||||
read_json(&dir.join("research-lock.json")).map(|lock| {
|
||||
@@ -401,10 +404,22 @@ fn resources_compatible(from: &Path, to: &Path, manifest: &serde_json::Value) ->
|
||||
let Some(fields) = manifest.as_object() else {
|
||||
return false;
|
||||
};
|
||||
let mut sources = vec![
|
||||
"tools/prepare_native_client.py".to_string(),
|
||||
"mods/horizon/src/test/java/dev/horizonlab/parity/VanillaHeightmapOracle.java".to_string(),
|
||||
];
|
||||
let mut preparer = vec![PathBuf::from("tools/prepare_native_client.py")];
|
||||
for dir in [from, to] {
|
||||
preparer.extend(files_under(dir, Path::new(PREPARER_CRATE)));
|
||||
}
|
||||
let same_or_absent = |path: &Path| match (fs::read(from.join(path)), fs::read(to.join(path))) {
|
||||
(Ok(a), Ok(b)) => a == b,
|
||||
(Err(a), Err(b)) => {
|
||||
a.kind() == std::io::ErrorKind::NotFound && b.kind() == std::io::ErrorKind::NotFound
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !preparer.iter().all(|p| same_or_absent(p)) {
|
||||
return false;
|
||||
}
|
||||
let mut sources =
|
||||
vec!["mods/horizon/src/test/java/dev/horizonlab/parity/VanillaHeightmapOracle.java".to_string()];
|
||||
for (_, value) in fields
|
||||
.iter()
|
||||
.filter(|(key, _)| key.ends_with("_exporters_sha256"))
|
||||
@@ -425,6 +440,26 @@ fn resources_compatible(from: &Path, to: &Path, manifest: &serde_json::Value) ->
|
||||
sources.iter().all(|s| same(s))
|
||||
}
|
||||
|
||||
/// Where a version folder keeps the preparer's sources (packaging/package.sh copies them).
|
||||
const PREPARER_CRATE: &str = "crates/horizon-prepare";
|
||||
|
||||
/// The files under `dir/relative`, as paths relative to `dir`.
|
||||
fn files_under(dir: &Path, relative: &Path) -> Vec<PathBuf> {
|
||||
let Ok(entries) = fs::read_dir(dir.join(relative)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut files = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let path = relative.join(entry.file_name());
|
||||
match entry.file_type() {
|
||||
Ok(t) if t.is_dir() => files.extend(files_under(dir, &path)),
|
||||
Ok(_) => files.push(path),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
fn copy_dir(from: &Path, to: &Path) -> std::io::Result<()> {
|
||||
fs::create_dir_all(to)?;
|
||||
for entry in fs::read_dir(from)? {
|
||||
@@ -500,7 +535,7 @@ fn reuse_prepared_in(versions: &Path) -> Vec<(String, String)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The official launcher's game directory, where prepare_native_client.py looks by default.
|
||||
/// The official launcher's game directory, where horizon-prepare looks by default.
|
||||
pub fn default_minecraft_dir() -> Option<PathBuf> {
|
||||
let home = dirs::home_dir()?;
|
||||
let dir = if cfg!(target_os = "macos") {
|
||||
@@ -531,13 +566,29 @@ fn python() -> Option<&'static str> {
|
||||
candidates.iter().copied().find(|p| found(p, "--version"))
|
||||
}
|
||||
|
||||
/// Checks what the preparation needs and builds its command. PLAY.txt describes the same steps:
|
||||
/// the player's own launcher install, the server jar from that version's own json, Java and Python.
|
||||
pub fn prepare_command(
|
||||
tag: &str,
|
||||
minecraft: &Path,
|
||||
log: &dyn Fn(String),
|
||||
) -> Result<Command, String> {
|
||||
/// How a version's resources are prepared.
|
||||
pub enum Preparer {
|
||||
/// horizon-prepare, linked into the launcher: the version ships the same preparer sources.
|
||||
InProcess(horizon_prepare::Options),
|
||||
/// The version's own preparer: its horizon-prepare binary when its sources differ from the
|
||||
/// launcher's (a newer or older release), or the Python script of versions made before it.
|
||||
Process(Command),
|
||||
}
|
||||
|
||||
/// The version's horizon-prepare binary, when it ships one.
|
||||
fn preparer_binary(dir: &Path) -> Option<PathBuf> {
|
||||
let binary = dir.join(if cfg!(windows) {
|
||||
"horizon-prepare.exe"
|
||||
} else {
|
||||
"horizon-prepare"
|
||||
});
|
||||
binary.is_file().then_some(binary)
|
||||
}
|
||||
|
||||
/// Checks what the preparation needs and picks how to run it. PLAY.txt describes the same
|
||||
/// steps: the player's own launcher install, the server jar from that version's own json, and
|
||||
/// Java (and Python, for versions before horizon-prepare).
|
||||
pub fn preparer(tag: &str, minecraft: &Path, log: &dyn Fn(String)) -> Result<Preparer, String> {
|
||||
let version = game_version(tag);
|
||||
let manifest = minecraft
|
||||
.join("versions")
|
||||
@@ -549,12 +600,27 @@ pub fn prepare_command(
|
||||
minecraft.display()
|
||||
));
|
||||
}
|
||||
let python = python().ok_or("Python 3.10 or newer is needed. Install it and try again.")?;
|
||||
if !found("java", "-version") {
|
||||
return Err(
|
||||
"Java 25 is needed (a JDK, e.g. Eclipse Temurin 25). Install it and try again.".into(),
|
||||
);
|
||||
}
|
||||
let dir = version_dir(tag);
|
||||
let binary = preparer_binary(&dir);
|
||||
let python = if binary.is_some() {
|
||||
// The JDK the version pins, found as horizon-prepare finds it, so a missing one is
|
||||
// reported before the server jar is downloaded.
|
||||
let major = read_json(&dir.join("research-lock.json"))
|
||||
.and_then(|lock| lock["minecraft"]["java_major"].as_i64())
|
||||
.ok_or("The version's research-lock.json has no java_major")?;
|
||||
horizon_prepare::java::find(major)?;
|
||||
None
|
||||
} else {
|
||||
let python = python()
|
||||
.ok_or("This version prepares with Python: Python 3.10 or newer is needed. Install it, or pick a newer version.")?;
|
||||
if !found("java", "-version") {
|
||||
return Err(
|
||||
"Java 25 is needed (a JDK, e.g. Eclipse Temurin 25). Install it and try again."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Some(python)
|
||||
};
|
||||
let jar = data_dir()
|
||||
.join("server-jars")
|
||||
.join(format!("{version}.jar"));
|
||||
@@ -577,8 +643,29 @@ pub fn prepare_command(
|
||||
.and_then(|_| fs::rename(&partial, &jar))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
let mut cmd = Command::new(python);
|
||||
cmd.current_dir(version_dir(tag))
|
||||
if let Some(binary) = binary {
|
||||
if horizon_prepare::sources_sha256(&dir.join(PREPARER_CRATE)).as_deref()
|
||||
== Some(horizon_prepare::SOURCES_SHA256)
|
||||
{
|
||||
return Ok(Preparer::InProcess(horizon_prepare::Options {
|
||||
root: dir,
|
||||
import: true,
|
||||
minecraft_dir: Some(minecraft.to_path_buf()),
|
||||
server_jar: Some(jar),
|
||||
}));
|
||||
}
|
||||
let mut cmd = Command::new(binary);
|
||||
cmd.current_dir(&dir)
|
||||
.arg("--root")
|
||||
.arg(&dir)
|
||||
.args(["--prepare", "--server-jar"])
|
||||
.arg(&jar)
|
||||
.arg("--minecraft-dir")
|
||||
.arg(minecraft);
|
||||
return Ok(Preparer::Process(cmd));
|
||||
}
|
||||
let mut cmd = Command::new(python.unwrap());
|
||||
cmd.current_dir(&dir)
|
||||
.args([
|
||||
"-u",
|
||||
"tools/prepare_native_client.py",
|
||||
@@ -588,7 +675,20 @@ pub fn prepare_command(
|
||||
.arg(&jar)
|
||||
.arg("--minecraft-dir")
|
||||
.arg(minecraft);
|
||||
Ok(cmd)
|
||||
Ok(Preparer::Process(cmd))
|
||||
}
|
||||
|
||||
/// Runs `Preparer::InProcess` on this thread, each progress line to `log`. A panic is an
|
||||
/// error like any other, so the window never waits on a preparation that died.
|
||||
pub fn prepare_in_process(
|
||||
options: &horizon_prepare::Options,
|
||||
log: &dyn Fn(String),
|
||||
) -> Result<(), String> {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
horizon_prepare::prepare(options, &|line| log(line.to_string()))
|
||||
}))
|
||||
.unwrap_or_else(|_| Err("The preparation stopped unexpectedly".into()))
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Where the game keeps saves and options.txt: `HORIZON_HOME`, else `horizon` in the platform's
|
||||
@@ -1430,6 +1530,23 @@ mod tests {
|
||||
assert!(is_prepared("v2026.10.17"));
|
||||
assert!(!is_prepared("v2026.10.15") && !is_prepared("v2026.10.16"));
|
||||
assert!(reuse_prepared_in(&versions).is_empty());
|
||||
|
||||
// Versions with horizon-prepare: its sources are the preparer, so the same sources
|
||||
// reuse each other's resources, and a change or a version without them prepares again.
|
||||
let crate_file = "crates/horizon-prepare/src/lib.rs";
|
||||
release("v2026.10.18", "26.3");
|
||||
write("v2026.10.18", crate_file, "rust preparer");
|
||||
assert_eq!(reuse_prepared(&versions, "v2026.10.18"), None);
|
||||
prepared("v2026.10.18", "26.3");
|
||||
release("v2026.10.19", "26.3");
|
||||
write("v2026.10.19", crate_file, "rust preparer");
|
||||
assert_eq!(
|
||||
reuse_prepared(&versions, "v2026.10.19"),
|
||||
Some("v2026.10.18".into())
|
||||
);
|
||||
release("v2026.10.20", "26.3");
|
||||
write("v2026.10.20", crate_file, "rust preparer, changed");
|
||||
assert_eq!(reuse_prepared(&versions, "v2026.10.20"), None);
|
||||
let _ = fs::remove_dir_all(&versions);
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -205,8 +205,12 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
|
||||
Cmd::Prepare { minecraft, version } => {
|
||||
let tag = backend::resolve_version(version.as_deref())?;
|
||||
let minecraft = backend::minecraft_dir(minecraft)?;
|
||||
let cmd = backend::prepare_command(&tag, &minecraft, &|line| eprintln!("{line}"))?;
|
||||
attached(cmd)?;
|
||||
match backend::preparer(&tag, &minecraft, &|line| eprintln!("{line}"))? {
|
||||
backend::Preparer::Process(cmd) => attached(cmd)?,
|
||||
backend::Preparer::InProcess(options) => {
|
||||
backend::prepare_in_process(&options, &|line| println!("{line}"))?
|
||||
}
|
||||
}
|
||||
println!("{tag} is prepared.");
|
||||
}
|
||||
Cmd::Play { version } => attached(backend::client(&backend::resolve_version(
|
||||
|
||||
+14
-4
@@ -472,17 +472,27 @@ impl Launcher {
|
||||
self.prepare_log.clear();
|
||||
self.prepare_error = None;
|
||||
self.prepare_log
|
||||
.push_back("Checking Java, Python and your Minecraft install".into());
|
||||
.push_back("Checking Java and your Minecraft install".into());
|
||||
let tx = self.tx.clone();
|
||||
// The checks may download the server jar, so they run on the thread too.
|
||||
std::thread::spawn(move || {
|
||||
let log = |line| {
|
||||
let _ = tx.send(Msg::Line(Job::Prepare, line));
|
||||
};
|
||||
let result = backend::prepare_command(&tag, &minecraft, &log)
|
||||
.and_then(|cmd| backend::spawn(Job::Prepare, cmd, tx.clone()));
|
||||
let result = backend::preparer(&tag, &minecraft, &log).and_then(|p| match p {
|
||||
backend::Preparer::Process(cmd) => {
|
||||
backend::spawn(Job::Prepare, cmd, tx.clone()).map(|_running| false)
|
||||
}
|
||||
backend::Preparer::InProcess(options) => {
|
||||
backend::prepare_in_process(&options, &log).map(|_| true)
|
||||
}
|
||||
});
|
||||
match result {
|
||||
Ok(_running) => {}
|
||||
// A process sends its own Exit when it ends.
|
||||
Ok(false) => {}
|
||||
Ok(true) => {
|
||||
let _ = tx.send(Msg::Exit(Job::Prepare, Ok(())));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(Msg::Exit(Job::Prepare, Err(e)));
|
||||
}
|
||||
|
||||
+20
-5
@@ -24,7 +24,7 @@ fn tools() -> Value {
|
||||
"inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "description": "A release tag such as v2026.10.13, or \"latest\"." } }, "required": ["tag"] } },
|
||||
{ "name": "remove_version", "description": "Remove an installed Horizon version and its prepared resources.",
|
||||
"inputSchema": { "type": "object", "properties": { "tag": { "type": "string" } }, "required": ["tag"] } },
|
||||
{ "name": "prepare_resources", "description": "Prepare a version's resources from the player's own Minecraft install (needs Minecraft 26.3 played once in the official launcher, Java 25 and Python 3.10+). Can take several minutes; returns the end of the log.",
|
||||
{ "name": "prepare_resources", "description": "Prepare a version's resources from the player's own Minecraft install (needs Minecraft 26.3 played once in the official launcher and Java 25; versions before horizon-prepare also need Python 3.10+). Can take several minutes; returns the end of the log.",
|
||||
"inputSchema": { "type": "object", "properties": { "version": version, "minecraft_dir": { "type": "string", "description": "The official launcher's game folder. Default: the launcher's setting, else the platform default." } } } },
|
||||
{ "name": "launch_client", "description": "Start the Horizon client (a game window on the player's screen). Returns once it has started.",
|
||||
"inputSchema": { "type": "object", "properties": { "version": version } } },
|
||||
@@ -95,10 +95,25 @@ impl Session {
|
||||
let tag = backend::resolve_version(arg_str(args, "version"))?;
|
||||
let minecraft =
|
||||
backend::minecraft_dir(arg_str(args, "minecraft_dir").map(PathBuf::from))?;
|
||||
let cmd = backend::prepare_command(&tag, &minecraft, &|_| {})?;
|
||||
let run = Logged::start(Job::Prepare, cmd)?;
|
||||
let result = run.wait();
|
||||
let tail = run.tail(20).join("\n");
|
||||
let (result, tail) = match backend::preparer(&tag, &minecraft, &|_| {})? {
|
||||
backend::Preparer::Process(cmd) => {
|
||||
let run = Logged::start(Job::Prepare, cmd)?;
|
||||
let result = run.wait();
|
||||
(result, run.tail(20).join("\n"))
|
||||
}
|
||||
backend::Preparer::InProcess(options) => {
|
||||
let lines = std::cell::RefCell::new(std::collections::VecDeque::new());
|
||||
let result = backend::prepare_in_process(&options, &|line| {
|
||||
let mut lines = lines.borrow_mut();
|
||||
lines.push_back(line);
|
||||
if lines.len() > 20 {
|
||||
lines.pop_front();
|
||||
}
|
||||
});
|
||||
let tail = lines.into_inner().into_iter().collect::<Vec<_>>().join("\n");
|
||||
(result, tail)
|
||||
}
|
||||
};
|
||||
result
|
||||
.map(|_| format!("{tag} is prepared.\n{tail}"))
|
||||
.map_err(|e| format!("{e}\n{tail}"))
|
||||
|
||||
+4
-5
@@ -17,11 +17,10 @@ Downloaded this archive directly?
|
||||
More about Horizon: https://playhorizon.dev
|
||||
|
||||
To prepare this folder by hand you need Minecraft Java Edition 26.3, played
|
||||
once from the official launcher, Python 3.10+ and Java 25 (a JDK). Then,
|
||||
in this folder:
|
||||
python3 tools/prepare_native_client.py --prepare --server-jar PATH/TO/server.jar
|
||||
python3 tools/fetch_sounds.py (optional, for sound)
|
||||
(Windows: py instead of python3.) The 26.3 server jar is the
|
||||
once from the official launcher, and Java 25 (a JDK). Then, in this folder:
|
||||
./horizon-prepare --prepare --server-jar PATH/TO/server.jar
|
||||
python3 tools/fetch_sounds.py (optional, for sound; needs Python 3.10+)
|
||||
(Windows: horizon-prepare.exe, and py instead of python3.) The 26.3 server jar is the
|
||||
downloads.server.url in your launcher's versions/26.3/26.3.json. Add
|
||||
--minecraft-dir PATH if your launcher keeps the game elsewhere. Then run
|
||||
./horizon-client (horizon-client.exe on Windows; on macOS the first time:
|
||||
|
||||
+16
-11
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# packaging/package.sh BINARY OUTDIR: a playable Horizon folder with the game
|
||||
# binary, horizon-ctl (built beside it: cargo build --release -p horizon-client --bins),
|
||||
# the resource preparation tool and what it compiles, and PLAY.txt.
|
||||
# binary, horizon-ctl and horizon-prepare (built beside it: cargo build --release
|
||||
# -p horizon-client --bins -p horizon-prepare), the exporters horizon-prepare compiles,
|
||||
# its sources (the launcher compares them to its own), and PLAY.txt.
|
||||
# It holds none of Minecraft's files: players prepare those from their own copy.
|
||||
set -euo pipefail
|
||||
bin=$1
|
||||
@@ -9,14 +10,18 @@ out=$2
|
||||
repo=$(git -C "$(dirname "$0")" rev-parse --show-toplevel)
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
git -C "$repo" archive HEAD tools mods/horizon/src/test/java research-lock.json LICENSE | tar -x -C "$out"
|
||||
git -C "$repo" archive HEAD tools mods/horizon/src/test/java crates/horizon-prepare research-lock.json LICENSE \
|
||||
| tar -x -C "$out"
|
||||
cp "$bin" "$out/"
|
||||
# horizon-ctl, the command line and MCP server that play the game, sits beside the client.
|
||||
ctl="$(dirname "$bin")/horizon-ctl"
|
||||
case "$bin" in *.exe) ctl="$ctl.exe" ;; esac
|
||||
if [ ! -f "$ctl" ]; then
|
||||
echo "package.sh: $ctl is missing; build it too: cargo build --release -p horizon-client --bins" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$ctl" "$out/"
|
||||
# horizon-ctl, the command line and MCP server that play the game, and horizon-prepare, which
|
||||
# prepares the resources from the player's own Minecraft, sit beside the client.
|
||||
for tool in horizon-ctl horizon-prepare; do
|
||||
path="$(dirname "$bin")/$tool"
|
||||
case "$bin" in *.exe) path="$path.exe" ;; esac
|
||||
if [ ! -f "$path" ]; then
|
||||
echo "package.sh: $path is missing; build it too: cargo build --release -p horizon-client --bins -p horizon-prepare" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$path" "$out/"
|
||||
done
|
||||
cp "$repo/packaging/PLAY.txt" "$out/PLAY.txt"
|
||||
@@ -85,7 +85,7 @@ def main():
|
||||
if not args.binary.is_file():
|
||||
raise SystemExit(f'missing {args.binary}: run cargo build --release -p horizon-client')
|
||||
if not (args.resources / 'horizon-resources.json').is_file():
|
||||
raise SystemExit(f'missing prepared resources in {args.resources}: run tools/prepare_native_client.py --prepare')
|
||||
raise SystemExit(f'missing prepared resources in {args.resources}: run horizon-prepare --prepare (cargo run --release -p horizon-prepare -- --prepare)')
|
||||
if not (args.resources / 'assets/minecraft/sounds.json').is_file():
|
||||
print('warning: no sound assets; run tools/fetch_sounds.py for audio')
|
||||
|
||||
|
||||
+17
-446
@@ -1,457 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare private pinned resources for Horizon's native development client."""
|
||||
import argparse
|
||||
import json
|
||||
"""Prepare private pinned resources for Horizon's native development client.
|
||||
|
||||
The preparation itself is horizon-prepare (crates/horizon-prepare), the Rust port of what this
|
||||
script did; this wrapper keeps its command line for the development tools that call it. It runs
|
||||
the horizon-prepare beside this folder (a release folder), else builds it with cargo. Arguments
|
||||
are passed through: --prepare, --server-jar PATH, --minecraft-dir PATH.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zipfile
|
||||
try:
|
||||
from . import lab, test_rust_parity as base, test_worldgen_parity as worldgen, test_model_baking_parity as baking
|
||||
except ImportError:
|
||||
import lab
|
||||
import test_rust_parity as base
|
||||
import test_worldgen_parity as worldgen
|
||||
import test_model_baking_parity as baking
|
||||
import sys
|
||||
|
||||
|
||||
WORLDGEN_BUNDLES = {
|
||||
'noises': 'worldgen/noise', 'densities': 'worldgen/density_function', 'rules': 'worldgen/material_rule',
|
||||
'conditions': 'worldgen/material_condition', 'settings': 'worldgen/noise_settings', 'carvers': 'worldgen/carver',
|
||||
'biomes': 'worldgen/biome', 'world_presets': 'worldgen/world_preset', 'features': 'worldgen/feature',
|
||||
'placed_features': 'worldgen/placed_feature', 'state_providers': 'worldgen/block_state_provider',
|
||||
'biome_tags': 'tags/worldgen/biome', 'feature_tags': 'tags/worldgen/feature', 'block_tags': 'tags/block',
|
||||
'fluid_tags': 'tags/fluid', 'processor_lists': 'worldgen/processor_list',
|
||||
'template_pools': 'worldgen/template_pool', 'structures': 'worldgen/structure',
|
||||
'structure_sets': 'worldgen/structure_set'}
|
||||
|
||||
|
||||
def export_structure_blocks(target, jars, libraries):
|
||||
"""Run the vanilla exporter of per-state rotate/mirror results and template flags."""
|
||||
lock = json.loads((lab.ROOT/'research-lock.json').read_text())
|
||||
source = lab.ROOT/'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaStructureBlockExporter.java'
|
||||
export = lab.LOCAL/'horizon-client-structures'
|
||||
classes = export/'classes'
|
||||
classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (export/'export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-cp', dependencies,
|
||||
'-d', str(classes), str(source)], check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes)+os.pathsep+dependencies,
|
||||
'dev.horizonlab.parity.VanillaStructureBlockExporter', str(target)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
|
||||
|
||||
BLOCK_BEHAVIOR_FILES = ['block-properties.nbt', 'cached-collision.nbt', 'cached-support.nbt', 'survival-rules.json',
|
||||
'survival-extra-definitions.json', 'placement-definitions.json']
|
||||
|
||||
|
||||
def export_block_behavior(export, jars, libraries):
|
||||
"""Run the vanilla exporters of block properties, support, collision and survival definitions.
|
||||
|
||||
Feature placement needs them: without these files the generator skips every placed feature
|
||||
whose blocks are checked for survival or support (trees, flowers, grass, kelp, ...)."""
|
||||
try:
|
||||
from . import test_block_survival_parity as survival
|
||||
except ImportError:
|
||||
import test_block_survival_parity as survival
|
||||
lock = json.loads((lab.ROOT/'research-lock.json').read_text())
|
||||
tests = lab.ROOT/'mods/horizon/src/test/java'
|
||||
classes = export/'classes'
|
||||
classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (export/'export.log').open('w') as log:
|
||||
# The oracle's helper classes are compiled on demand from the source path.
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-cp', dependencies,
|
||||
'-sourcepath', str(tests), '-d', str(classes),
|
||||
str(tests/'dev/horizonlab/parity/VanillaSimpleBlockOracle.java')],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes)+os.pathsep+dependencies,
|
||||
'dev.horizonlab.parity.VanillaSimpleBlockOracle', 'export', str(export)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
rules = survival.bind_definitions(*[json.loads((export/name).read_text()) for name in
|
||||
['survival-classes.json', 'survival-placement-classes.json',
|
||||
'survival-support-tags.json']])
|
||||
(export/'survival-rules.json').write_text(json.dumps(rules, separators=(',', ':')))
|
||||
|
||||
|
||||
def prepare_worldgen(root, jars, local, libraries=None):
|
||||
"""Write the chunk generator's resource bundle to root/worldgen (see GeneratorData::load)."""
|
||||
out = root / 'worldgen'
|
||||
out.mkdir(exist_ok=True)
|
||||
# Structure templates: gzip NBT files under structure/<namespace>/<path>.nbt.
|
||||
with zipfile.ZipFile(jars['server']) as server:
|
||||
prefix = 'data/minecraft/structure/'
|
||||
for name in server.namelist():
|
||||
if name.startswith(prefix) and name.endswith('.nbt'):
|
||||
relative = Path(name[len(prefix):])
|
||||
if relative.is_absolute() or '..' in relative.parts:
|
||||
raise ValueError('Invalid structure path')
|
||||
target = out/'structure'/'minecraft'/relative
|
||||
data = server.read(name)
|
||||
if not target.exists() or target.read_bytes() != data:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
with zipfile.ZipFile(jars['server']) as server, zipfile.ZipFile(jars['client']) as client:
|
||||
for name, path in WORLDGEN_BUNDLES.items():
|
||||
prefix = f'data/minecraft/{path}/'
|
||||
def extract(archive):
|
||||
return {'minecraft:'+n[len(prefix):-5]: archive.read(n).decode('utf-8')
|
||||
for n in sorted(archive.namelist()) if n.startswith(prefix) and n.endswith('.json')}
|
||||
resources = extract(server)
|
||||
if not resources or resources != extract(client):
|
||||
raise ValueError('Reference worldgen resources disagree: '+path)
|
||||
(out/(name+'.json')).write_text(json.dumps(resources, separators=(',', ':')), encoding='utf-8')
|
||||
facts = json.loads((root/'facts.json').read_text())
|
||||
(out/'block_flags.json').write_text(json.dumps([[s[1], s[2]] for s in facts['states']], separators=(',', ':')))
|
||||
reports = out/'biome_parameters'
|
||||
reports.mkdir(exist_ok=True)
|
||||
presets = reports
|
||||
for source in [root/'biome-parameters', local/'datagen/out/reports/biome_parameters/minecraft']:
|
||||
if (source/'overworld.json').exists():
|
||||
for report in sorted(source.glob('*.json')):
|
||||
(reports/report.name).write_bytes(report.read_bytes())
|
||||
break
|
||||
else:
|
||||
presets = None
|
||||
# Block behavior used by feature placement checks (survival, support, collision).
|
||||
# These come from the Java exporters of the simple-block parity run, or a direct export.
|
||||
behavior = local/'rust-simple-block-parity'
|
||||
blocks = out/'blocks'
|
||||
names = BLOCK_BEHAVIOR_FILES
|
||||
if libraries is not None and not all((behavior/name).exists() for name in names):
|
||||
behavior = local/'horizon-client-block-behavior'
|
||||
export_block_behavior(behavior, jars, libraries)
|
||||
if all((behavior/name).exists() for name in names):
|
||||
blocks.mkdir(exist_ok=True)
|
||||
for name in names:
|
||||
(blocks/name).write_bytes((behavior/name).read_bytes())
|
||||
if libraries is not None:
|
||||
blocks.mkdir(exist_ok=True)
|
||||
export_structure_blocks(blocks/'structure-blocks.json', jars, libraries)
|
||||
else:
|
||||
print('Feature block behavior exports missing; run tools/test_simple_block_parity.py for full decoration')
|
||||
if presets is None:
|
||||
# Fall back to the Java-exported quantized tables from the material-stage parity run.
|
||||
presets = local/'rust-material-stage-parity/presets.json'
|
||||
if not presets.exists():
|
||||
raise FileNotFoundError('No multi-noise biome parameter tables; run the data generator reports')
|
||||
(out/'presets.json').write_bytes(presets.read_bytes())
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--prepare', action='store_true',
|
||||
help='Import the pinned game files from your own copy first')
|
||||
lab.add_game_arguments(parser)
|
||||
args = parser.parse_args()
|
||||
if args.prepare:
|
||||
lab.prepare(args.minecraft_dir, args.server_jar)
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
root = lab.LOCAL / 'horizon-client' / lock['minecraft']['version']
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
facts = root / 'facts.json'
|
||||
manifest = root / 'horizon-resources.json'
|
||||
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaHeightmapOracle.java'
|
||||
origin = {'minecraft': lock['minecraft']['version'],
|
||||
'client_sha256': lab.digest(jars['client'], 'sha256'),
|
||||
'server_sha256': lab.digest(jars['server'], 'sha256'),
|
||||
'exporter_sha256': lab.digest(oracle, 'sha256')}
|
||||
previous = json.loads(manifest.read_text()) if manifest.exists() else {}
|
||||
if not (all(previous.get(k) == v for k, v in origin.items()) and facts.exists()
|
||||
and previous.get('facts_sha256') == lab.digest(facts, 'sha256')):
|
||||
classes = root / 'classes'
|
||||
classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (root / 'export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(oracle)], check=True,
|
||||
stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx512M', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaHeightmapOracle', 'export', str(facts)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
binding = oracle.with_name('VanillaBlockStateBindingOracle.java')
|
||||
helper = oracle.with_name('VanillaCuboidModelOracle.java')
|
||||
binding_sources = base.source_hashes([binding, helper])
|
||||
model_states = root / 'model-states.json'
|
||||
if not (previous.get('model_state_exporters_sha256') == binding_sources
|
||||
and previous.get('client_sha256') == origin['client_sha256']
|
||||
and model_states.exists()
|
||||
and previous.get('model_states_sha256') == lab.digest(model_states, 'sha256')):
|
||||
classes = root / 'client-classes'; classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['client'], *baking.client_libraries()]))
|
||||
with (root / 'model-state-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(binding), str(helper)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx512M', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaBlockStateBindingOracle', 'export', str(model_states)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
tags = worldgen.tag_resources(jars['server'])
|
||||
if tags != worldgen.tag_resources(jars['client']):
|
||||
raise ValueError('Reference block tags disagree')
|
||||
lab.write_json(root / 'block-tags.json', tags)
|
||||
visibility = oracle.with_name('VanillaFaceVisibilityOracle.java')
|
||||
collision = oracle.with_name('VanillaCollisionOracle.java')
|
||||
collision_sources = base.source_hashes([collision])
|
||||
movement = oracle.with_name('VanillaMovementRegistryOracle.java')
|
||||
attributes = oracle.with_name('VanillaEntityAttributeOracle.java')
|
||||
landing = oracle.with_name('VanillaBlockLandingOracle.java')
|
||||
motion_sources = base.source_hashes([movement, attributes, oracle, landing, collision])
|
||||
motion_files = ['movement-properties.nbt', 'attributes.json', 'landing-definitions.json']
|
||||
if not (previous.get('motion_exporters_sha256') == motion_sources
|
||||
and previous.get('server_sha256') == origin['server_sha256']
|
||||
and previous.get('block_tags_sha256') == lab.digest(root/'block-tags.json', 'sha256')
|
||||
and all((root/name).exists() and previous.get('motion_files_sha256', {}).get(name)
|
||||
== lab.digest(root/name, 'sha256') for name in motion_files)):
|
||||
export = lab.LOCAL/'horizon-client-motion'; export.mkdir(exist_ok=True)
|
||||
lab.write_json(export/'tags.json', tags)
|
||||
classes = export/'classes'; classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (export/'export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(movement), str(attributes), str(oracle), str(landing), str(collision)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
for name in ['VanillaBlockLandingOracle', 'VanillaEntityAttributeOracle']:
|
||||
subprocess.run(['java', '-Xmx768M', '-cp', str(classes)+os.pathsep+dependencies,
|
||||
'dev.horizonlab.parity.'+name, 'export', str(export)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
(root/'movement-properties.nbt').write_bytes((export/'movement-properties.nbt').read_bytes())
|
||||
(root/'attributes.json').write_bytes((export/'catalog.json').read_bytes())
|
||||
(root/'landing-definitions.json').write_bytes((export/'landing-definitions.json').read_bytes())
|
||||
damage = oracle.with_name('VanillaDamageSourceOracle.java')
|
||||
health = oracle.with_name('VanillaPlayerHealthOracle.java')
|
||||
health_sources = base.source_hashes([damage, health, attributes])
|
||||
health_files = ['damage-types.json', 'damage-tags.json', 'damage-catalog.json', 'player-float-ids.json']
|
||||
if not (previous.get('health_exporters_sha256') == health_sources
|
||||
and previous.get('server_sha256') == origin['server_sha256']
|
||||
and all((root/name).exists() and previous.get('health_files_sha256', {}).get(name)
|
||||
== lab.digest(root/name, 'sha256') for name in health_files)):
|
||||
export = lab.LOCAL/'horizon-client-health'; export.mkdir(exist_ok=True)
|
||||
with zipfile.ZipFile(jars['server']) as archive, zipfile.ZipFile(jars['client']) as client_archive:
|
||||
for file, prefix in [('types.json', 'data/minecraft/damage_type/'), ('tags.json', 'data/minecraft/tags/damage_type/')]:
|
||||
def extract(source):
|
||||
return {'minecraft:'+n[len(prefix):-5]: source.read(n).decode('utf-8')
|
||||
for n in sorted(source.namelist()) if n.startswith(prefix) and n.endswith('.json')}
|
||||
resources = extract(archive)
|
||||
if resources != extract(client_archive): raise ValueError('Reference damage resources disagree')
|
||||
lab.write_json(export/file, resources)
|
||||
classes = export/'classes'; classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (export/'export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(damage), str(health), str(attributes)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
for name in ['VanillaPlayerHealthOracle', 'VanillaDamageSourceOracle']:
|
||||
subprocess.run(['java', '-Xmx768M', '-cp', str(classes)+os.pathsep+dependencies,
|
||||
'dev.horizonlab.parity.'+name, 'export', str(export)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
for source, target in [('types.json','damage-types.json'), ('tags.json','damage-tags.json'),
|
||||
('catalog.json','damage-catalog.json'), ('ids.json','player-float-ids.json')]:
|
||||
(root/target).write_bytes((export/source).read_bytes())
|
||||
collision_file = root / 'cached-collision.nbt'
|
||||
if not (previous.get('collision_exporters_sha256') == collision_sources
|
||||
and previous.get('server_sha256') == origin['server_sha256']
|
||||
and collision_file.exists()
|
||||
and previous.get('collision_sha256') == lab.digest(collision_file, 'sha256')):
|
||||
export = lab.LOCAL/'horizon-client-collision'; export.mkdir(exist_ok=True)
|
||||
classes = export/'classes'; classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (export/'export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(collision)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx768M', '-cp', str(classes)+os.pathsep+dependencies,
|
||||
'dev.horizonlab.parity.VanillaCollisionOracle', 'generate', str(export)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
collision_file.write_bytes((export/'cached-collision.nbt').read_bytes())
|
||||
render_sources = base.source_hashes([visibility, collision, oracle])
|
||||
render_files = ['render-definitions.json', 'occlusion.nbt']
|
||||
tag_hash = lab.digest(root/'block-tags.json', 'sha256')
|
||||
if not (previous.get('render_exporters_sha256') == render_sources
|
||||
and previous.get('server_sha256') == origin['server_sha256']
|
||||
and previous.get('block_tags_sha256') == tag_hash
|
||||
and all((root/name).exists() and previous.get('render_files_sha256', {}).get(name)
|
||||
== lab.digest(root/name, 'sha256') for name in render_files)):
|
||||
export = lab.LOCAL/'horizon-client-render'; export.mkdir(exist_ok=True)
|
||||
lab.write_json(export/'tags.json', tags)
|
||||
classes = export/'classes'; classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (export/'export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(visibility), str(collision), str(oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx768M', '-cp', str(classes)+os.pathsep+dependencies,
|
||||
'dev.horizonlab.parity.VanillaFaceVisibilityOracle', 'export', str(export)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
(root/'render-definitions.json').write_bytes((export/'definitions.json').read_bytes())
|
||||
(root/'occlusion.nbt').write_bytes((export/'occlusion.nbt').read_bytes())
|
||||
count = 0
|
||||
# The playable client needs every client asset (textures, models, fonts, GUI)
|
||||
# and every data-pack resource (worldgen, recipes, loot tables, tags).
|
||||
prefixes = ('assets/', 'data/')
|
||||
with zipfile.ZipFile(jars['client']) as archive:
|
||||
version = json.loads(archive.read('version.json'))
|
||||
for name in archive.namelist():
|
||||
if not name.startswith(prefixes) or name.endswith('/'):
|
||||
continue
|
||||
relative = Path(name)
|
||||
if relative.is_absolute() or '..' in relative.parts:
|
||||
raise ValueError('Invalid resource path')
|
||||
target = root / relative
|
||||
data = archive.read(name)
|
||||
if not target.exists() or target.read_bytes() != data:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
count += 1
|
||||
gameplay = root / 'gameplay-facts.json'
|
||||
gameplay_oracle = oracle.with_name('VanillaGameplayFactsExporter.java')
|
||||
gameplay_sources = base.source_hashes([gameplay_oracle])
|
||||
if not (previous.get('gameplay_exporters_sha256') == gameplay_sources and gameplay.exists()
|
||||
and previous.get('server_sha256') == origin['server_sha256']):
|
||||
classes = lab.LOCAL / 'gameplay-facts' / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (root / 'gameplay-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(gameplay_oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaGameplayFactsExporter', str(gameplay)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
# Entity dimensions, categories and default attributes for the client's mobs.
|
||||
entity_types = root / 'entity-types.json'
|
||||
entity_types_oracle = oracle.with_name('VanillaEntityTypesExporter.java')
|
||||
entity_types_sources = base.source_hashes([entity_types_oracle])
|
||||
if not (previous.get('entity_types_exporters_sha256') == entity_types_sources and entity_types.exists()
|
||||
and previous.get('server_sha256') == origin['server_sha256']):
|
||||
classes = lab.LOCAL / 'entity-types' / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (root / 'entity-types-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(entity_types_oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaEntityTypesExporter', str(entity_types)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
# Block classes, instance configuration and per-state support/signal/fluid
|
||||
# facts for horizon-world's level behaviour (level::Behaviours::load).
|
||||
behaviours = root / 'block-behaviours.json'
|
||||
behaviour_oracle = oracle.with_name('VanillaBlockBehaviourExporter.java')
|
||||
behaviour_sources = base.source_hashes([behaviour_oracle])
|
||||
if not (previous.get('behaviour_exporters_sha256') == behaviour_sources and behaviours.exists()
|
||||
and previous.get('server_sha256') == origin['server_sha256']):
|
||||
classes = lab.LOCAL / 'block-behaviours' / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (root / 'behaviour-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(behaviour_oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaBlockBehaviourExporter', str(behaviours)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
items = root / 'items.json'
|
||||
if not items.exists() or previous.get('server_sha256') != origin['server_sha256']:
|
||||
# Vanilla's own data generator reports every item's default components.
|
||||
datagen = lab.LOCAL / 'datagen'; datagen.mkdir(exist_ok=True)
|
||||
with (datagen / 'datagen.log').open('w') as log:
|
||||
subprocess.run(['java', '-DbundlerMainClass=net.minecraft.data.Main', '-jar',
|
||||
str(jars['server'].with_name('server.jar')), '--reports', '--output', 'out'],
|
||||
cwd=datagen, check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
reports = datagen / 'out' / 'reports'
|
||||
components = {f'minecraft:{f.stem}': json.loads(f.read_text())['components']
|
||||
for f in sorted((reports / 'minecraft' / 'components' / 'item').glob('*.json'))}
|
||||
items.write_text(json.dumps(components))
|
||||
(root / 'biome-parameters').mkdir(exist_ok=True)
|
||||
for f in (reports / 'biome_parameters' / 'minecraft').glob('*.json'):
|
||||
(root / 'biome-parameters' / f.name).write_bytes(f.read_bytes())
|
||||
# Biome parameter tables come from the data generator reports above.
|
||||
prepare_worldgen(root, jars, lab.LOCAL, libraries)
|
||||
entity_models = root / 'entity-models.json'
|
||||
entity_oracle = oracle.with_name('VanillaEntityModelExporter.java')
|
||||
entity_sources = base.source_hashes([entity_oracle])
|
||||
if not (previous.get('entity_model_exporters_sha256') == entity_sources and entity_models.exists()
|
||||
and previous.get('client_sha256') == origin['client_sha256']):
|
||||
classes = lab.LOCAL / 'entity-models' / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['client'], *baking.client_libraries()]))
|
||||
with (root / 'entity-model-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(entity_oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaEntityModelExporter', str(entity_models)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
# Mob AI and animation facts: per-state path types, spawn placements and the
|
||||
# client's keyframe animation definitions (needs the client jar).
|
||||
mob_data = root / 'mob-data.json'
|
||||
mob_oracle = oracle.with_name('VanillaMobDataExporter.java')
|
||||
mob_sources = base.source_hashes([mob_oracle])
|
||||
if not (previous.get('mob_data_exporters_sha256') == mob_sources and mob_data.exists()
|
||||
and previous.get('client_sha256') == origin['client_sha256']):
|
||||
classes = lab.LOCAL / 'mob-data' / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['client'], *baking.client_libraries()]))
|
||||
with (root / 'mob-data-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(mob_oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaMobDataExporter', str(mob_data), str(jars['client'])],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
# Vanilla's built-in registries for in-game command parsing (mob effects,
|
||||
# data component types, dialogs, block entity and tag members).
|
||||
command_registries = root / 'command-registries.json'
|
||||
command_oracle = oracle.with_name('VanillaCommandParseOracle.java')
|
||||
command_sources = base.source_hashes([command_oracle])
|
||||
if not (previous.get('command_registries_exporters_sha256') == command_sources and command_registries.exists()
|
||||
and previous.get('server_sha256') == origin['server_sha256']):
|
||||
export = lab.LOCAL / 'command-registries'; classes = export / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (root / 'command-registries-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), str(command_oracle)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaCommandParseOracle', 'registries', str(export)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
command_registries.write_bytes((export / 'registries.json').read_bytes())
|
||||
# The creative inventory (creative-inventory.json): every tab built by vanilla's
|
||||
# CreativeModeTabs over the vanilla data pack's registries, with the search
|
||||
# tab's tooltip lines and tags.
|
||||
creative = root / 'creative-inventory.json'
|
||||
creative_oracles = [oracle.with_name('VanillaCreativeTabsExporter.java'), oracle.with_name('ParityData.java')]
|
||||
creative_sources = base.source_hashes(creative_oracles)
|
||||
if not (previous.get('creative_exporters_sha256') == creative_sources and creative.exists()
|
||||
and previous.get('server_sha256') == origin['server_sha256']):
|
||||
classes = lab.LOCAL / 'creative-inventory' / 'classes'; classes.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
with (root / 'creative-export.log').open('w') as log:
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none',
|
||||
'-cp', dependencies, '-d', str(classes), *map(str, creative_oracles)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
subprocess.run(['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaCreativeTabsExporter', str(creative)],
|
||||
check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600)
|
||||
lab.write_json(manifest, {'schema': 7, **origin,
|
||||
'health_exporters_sha256': health_sources,
|
||||
'health_files_sha256': {name: lab.digest(root/name, 'sha256') for name in health_files},
|
||||
'motion_exporters_sha256': motion_sources,
|
||||
'motion_files_sha256': {name: lab.digest(root/name, 'sha256') for name in motion_files},
|
||||
'collision_exporters_sha256': collision_sources,
|
||||
'collision_sha256': lab.digest(collision_file, 'sha256'),
|
||||
'render_exporters_sha256': render_sources,
|
||||
'render_files_sha256': {name: lab.digest(root/name, 'sha256') for name in render_files},
|
||||
'model_state_exporters_sha256': binding_sources,
|
||||
'gameplay_exporters_sha256': gameplay_sources,
|
||||
'entity_model_exporters_sha256': entity_sources,
|
||||
'creative_exporters_sha256': creative_sources,
|
||||
'mob_data_exporters_sha256': mob_sources,
|
||||
'entity_types_exporters_sha256': entity_types_sources,
|
||||
'behaviour_exporters_sha256': behaviour_sources,
|
||||
'command_registries_exporters_sha256': command_sources,
|
||||
'model_states_sha256': lab.digest(model_states, 'sha256'), 'data_version': version['world_version'],
|
||||
'facts_sha256': lab.digest(facts, 'sha256'),
|
||||
'block_tags_sha256': lab.digest(root / 'block-tags.json', 'sha256'), 'resource_files': count})
|
||||
print(f'Prepared Horizon native-client resources: {root}')
|
||||
arguments = ['--root', str(ROOT), *sys.argv[1:]]
|
||||
beside = ROOT / ('horizon-prepare.exe' if os.name == 'nt' else 'horizon-prepare')
|
||||
if beside.is_file():
|
||||
command = [str(beside), *arguments]
|
||||
else:
|
||||
command = ['cargo', 'run', '--quiet', '--release', '-p', 'horizon-prepare', '--', *arguments]
|
||||
return subprocess.call(command, cwd=ROOT)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(main())
|
||||
@@ -45,7 +45,7 @@ def target(game_dir, name):
|
||||
"""REA admits JADX targets by their .apk name; JADX reads the class files either way."""
|
||||
source = game_dir / "jars" / f"{name}.jar"
|
||||
if not source.exists():
|
||||
raise SystemExit(f"{source} is missing; run python3 tools/prepare_native_client.py --prepare")
|
||||
raise SystemExit(f"{source} is missing; run cargo run --release -p horizon-prepare -- --prepare")
|
||||
copy = lab.LOCAL / "rea" / f"{name}-{lab.digest(source, 'sha256')[:16]}.apk"
|
||||
if not copy.exists():
|
||||
copy.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Compare the creative inventory's tabs and search with the vanilla client's.
|
||||
|
||||
The Java oracle (VanillaCreativeSearchOracle) writes the creative-inventory.json
|
||||
that tools/prepare_native_client.py exports (VanillaCreativeTabsExporter: every
|
||||
that horizon-prepare exports (VanillaCreativeTabsExporter: every
|
||||
tab built by vanilla's CreativeModeTabs over the vanilla data pack's
|
||||
registries), then what the vanilla client shows: every tab's display items
|
||||
and, for each search query, CreativeModeInventoryScreen.refreshSearchResults'
|
||||
|
||||
Reference in new issue
Block a user