Compare commits

...
Author SHA1 Message Date
levzzz ddb317c771 Merge branch 'pr-5909-new' into mod-api-rework 2026-11-01 20:40:22 +05:00
[email protected]andClaude Opus 5.5 aef97f1e43 Merge upstream/main into staging/unified-hmod
Upstream added --player-dir handling to the startup code in main.rs. With the
library split that code lives in entry.rs, so the change is applied there and
main.rs stays the thin launcher.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 16:35:47 +01:00
levzzz 3f3bdfa663 Merge branch 'pr-5909' into mod-api-rework 2026-11-01 20:24:21 +05:00
levzzzandClaude Opus 5.5 5482b975fb Merge hr/main into the mod API rework (#5446)
main.rs moved to entry.rs on this branch; carry main's --player-dir
log/crash folder handling over to entry.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 20:18:53 +05:00
[email protected]andClaude Opus 5.5 18a02fe574 Merge native/api-mods (#5446) into staging/unified-hmod
Combined staging branch: the game library split (#5446) and the single mod
file format (#5909) together, as agreed to land in one release. No conflicts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 16:17:21 +01:00
[email protected]andClaude Opus 5.5 8421d34846 Merge upstream/main into native/api-mods
Upstream added `pub mod lod_net;` to the old lib.rs module list, which this PR
moved to client_lib.rs: add it there. Upstream's new horizon-optifine
dependency is used by mobs/cem.rs, a reusable-client module, so it goes in
horizon-client-lib's manifest instead of horizon-client's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 16:03:03 +01:00
[email protected]andCodex 9e18079acc fix(parity): migrate broad saved client target selections too
Co-Authored-By: Codex <[email protected]>
2026-11-01 14:41:20 +01:00
[email protected]andClaude Opus 5.5 cb11c1d221 Block states: drop a needless borrow clippy 1.99 refuses
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 13:45:12 +01:00
[email protected] 2aa6aa28cd Merge remote-tracking branch 'upstream/main' into staging/unified-hmod 2026-11-01 13:43:44 +01:00
[email protected]andClaude Opus 5.5 df6e0f0547 Mods: keep publishing .hnmod, write the mod file format down
Review on #5909: native versions published as .hmod would reach
launchers that drop their content or refuse them. The registry keeps
.hnmod until launchers and the game read native .hmods.

docs/api/mod-files.md holds every rule of a .hmod/.hnmod in one
place: zip and path rules, top-level entries, the horizon-native.json
schema, what mixins/<id>/ may hold and the size limits.

The registry's Cargo.lock catches up with horizon-mods' png
dependency so its --locked check passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 13:35:43 +01:00
[email protected]andClaude Opus 5.5 f2b508c4f2 test(client): read the loot hook source independent of line endings
loot_table_load_stays_hookable searched for "\n}\n", which a CRLF checkout
(core.autocrlf=true on Windows) never contains. Normalize as the other
source-reading test in this file does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 13:11:46 +01:00
[email protected]andClaude Opus 5.5 02122e2532 Mods: one .hmod for content and native code (WIP, #5865)
A .hmod's contents decide what it is: content only, native code
(horizon-native.json + mixins/<id>/), or both in one file. Native
bundle reading skips a .hmod's content entries, checks both
manifests name one mod, and refuses mod.wasm with a clear error.
The registry publishes native-mod versions as .hmod and still
accepts .hnmod; .hnmod keeps loading unchanged.

Launcher install, game content loading of native .hmods and the
GitHub install path are not done yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 13:04:52 +01:00
[email protected]andCodex 452910ffce fix(parity): keep saved probe selections compatible with the library split
Co-Authored-By: Codex <[email protected]>
2026-11-01 12:17:53 +01:00
[email protected]andCodex 42a5981e94 refactor(client): isolate gameplay from the reusable client and probes
Keep native hook targets and source paths stable. Run gameplay tests in the gameplay library, and build ctl/probes from horizon-client-lib without gameplay. Update build/test selection and the fixed mixin-tool pin.

Co-Authored-By: Codex <[email protected]>
2026-11-01 12:05:47 +01:00
[email protected]andCodex 2c78a5f1b0 Merge upstream/main before revising the client library layout
Co-Authored-By: Codex <[email protected]>
2026-11-01 11:50:56 +01:00
[email protected]andCodex a303d7d105 Merge upstream/main into the client library split
Retain upstream block_contact and the split entry/game exports with test isolation.

Co-Authored-By: Codex <[email protected]>
2026-11-01 10:15:44 +01:00
[email protected]andGPT-6 cd85269d59 docs: correct parity selection costs after the client split
Document refreshed probe dep-info, conservative selection growth, and existing reachability narrowing without promising the old harness counts.

Co-Authored-By: GPT-6 <[email protected]>
2026-10-31 23:31:27 +01:00
[email protected]andGPT-6 022306f5d8 refactor(client): keep the library split without the separate mod API
Co-Authored-By: GPT-6 <[email protected]>
2026-10-31 23:03:46 +01:00
rdbtCVSandClaude Opus 5.5 60851159b2 Merge main into #5446
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 21:50:15 +00:00
[email protected]andClaude Opus 5.5 1e0fe9bb79 Merge upstream/main; the game's unit tests keep their own process
docs/native-mods.md keeps main's API 58 notes, then the API mods section.

With the game module in the horizon-client library, its unit tests ran in the
library's test process and shared process-wide state with the library's tests
(health::HURT_SERVER, the sulfur cube archetypes): six tests failed. The game
module now builds into the library only outside tests; its unit tests stay in the
executable's test build, as before the move.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 21:36:47 +01:00
[email protected]andClaude Opus 5.5 af806fd1c3 Merge upstream/main into native/api-mods
Conflicts: game.rs keeps the api_mods export beside main's narrator module;
horizon-threads takes main's windows-sys features (the same Windows fix);
native-mods.md keeps main's API 47-57 notes, then the API mods section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 21:09:44 +01:00
[email protected] 0fb25759e1 boot_paint: AddFontMemResourceEx only reads the font count
Clippy (unnecessary_mut_passed) now that boot_paint compiles on Windows.
2026-10-31 20:25:15 +01:00
[email protected] 3888ac4613 Native mods without mixins: API mods that never rebuild horizon-client
A native bundle with [api] (crate and entry) and no [[injection]] is an API mod: its
crate implements horizon_mod_api::Mod and the game asks it right after the item and
block hook points it is named after, once the mixin mods there are done.

- horizon-mod-api: the Mod trait (use_item, use_item_on, right_click_block,
  left_click_block, destroy_block, place_block, tooltip), Action and Host.
- horizon-client: the game module moves into the library and the executable only adds
  the allocator, so a build can link the game from another package; game/api_mods.rs
  dispatches, and a mod turned off in Options > Mods is skipped with no build.
- launcher: API mods skip horizon-mixins; their crate and a generated native-game
  package build the executable, so horizon-client never depends on a mod.
- horizon-mods: the manifest's optional api field and its checks.
- docs: native-mods.md "API mods (no mixins)", stability.md; example in
  launcher/tests/fixtures/native-api-demo.
2026-10-31 20:25:15 +01:00
[email protected] e2d852088b Windows build: the windows-sys features boot_paint uses
boot_paint.rs draws the boot screen with GDI (Win32_Graphics_Gdi) and reads the
window's size (Win32_UI_WindowsAndMessaging), which horizon-threads didn't turn on.
2026-10-31 19:47:15 +01:00
118 changed files with 1139 additions and 384 deletions

No files matched your search

+1 -1
View File
@@ -150,7 +150,7 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
+15 -8
View File
@@ -39,7 +39,8 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~100 flat modules: gameplay, networking, title screens) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-client` | Gameplay library (`src/lib.rs`: `game.rs` + `game/`, `entry.rs`) and thin executable (`src/main.rs`). Also the dedicated server (`--dedicated`). Re-exports `horizon-client-lib` for existing mod paths. |
| `horizon-client-lib` | Reusable client systems, `horizon-ctl`, probes and their tests. Its manifest points to the existing sources under `crates/horizon-client/`; the library root is `src/client_lib.rs`. It never depends on gameplay. |
| `horizon-items` | Blocks and items below gameplay: `blocks.rs` (block facts), `items.rs`, `item_data.rs` (components), `enchant.rs`, `cooldowns.rs`, `bundle.rs`, `item_nbt.rs`. Re-exported as `horizon_client::items`... |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
@@ -84,8 +85,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
@@ -102,8 +103,11 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
or the game binary with a fixture env var (`game/dispense_fixture.rs`, `game/sculk_fixture.rs`,
`game/command_harness.rs`, `game/net_harness.rs`). Copy a nearby harness for a new one.
- Probes build into `.local/parity-target/`; harnesses find binaries via `lab.cargo_target()`, never `target/`.
- What a change reruns (of 329 harnesses, measured 2026-10-23): a file of the game binary (`game.rs`, `game/`)
~25, a file of the client lib ~103, of `horizon-world` ~103–109, of `horizon-core` ~119. Plan for it.
- Harness selection uses Cargo dep-info and reachable-code narrowing (`tools/parity_reach.py`).
`horizon-client-lib` builds the control tool and client probes without gameplay; their dep-info
excludes `game.rs` and `game/`, so gameplay edits do not rebuild or select those probes.
Source paths remain in `crates/horizon-client/`. Measure actual harness selection on refreshed
records with `tools/parity.py --changed-files paths.txt --list`; historical counts are not a guarantee.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
@@ -128,8 +132,11 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`, or `crates/horizon-client-render/src/` for the modules that crate holds, under
the same `horizon_client::<module>` paths), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
the same `horizon_client::<module>` paths), `G:` = gameplay (`crates/horizon-client/src/game/`), also in the lib in normal builds.
Gameplay keeps its `horizon_client::<module>` imports through a self-crate alias. The executable
contains only the allocator, mod API marker and a call to `entry::main`. For unit tests, the lib
excludes `entry` and `game`, and the executable test target owns `game` so its process-wide test
state stays separate from the library tests. This compiles gameplay twice for executable tests.
| Subsystem | Files |
|---|---|
@@ -160,7 +167,7 @@ the same `horizon_client::<module>` paths), `G:` = bin (`crates/horizon-client/s
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
the core (tick, render, input, inventory, world open/close) and each `game/<feature>.rs` adds an
`impl Game<'_>` block for one feature, with its own state types. Fields are private to the bin.
`impl Game<'_>` block for one feature, with its own state types. Fields stay private to the gameplay module and its descendants. Gameplay tests run in the gameplay library, separate from reusable-library tests.
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
the file is usually `game/<thing>s.rs`. Who calls it: grep the name in game.rs's `fn tick` / `fn tick_world`.
- Fields that say which mode runs: `remote` (this window is a client of a server: multiplayer, LAN guest, or
Generated
+57 -1
View File
@@ -1314,6 +1314,7 @@ dependencies = [
"horizon-chat-filter",
"horizon-client-core",
"horizon-client-input",
"horizon-client-lib",
"horizon-client-render",
"horizon-command",
"horizon-core",
@@ -1323,7 +1324,6 @@ dependencies = [
"horizon-items",
"horizon-mods",
"horizon-narrator",
"horizon-optifine",
"horizon-protocol",
"horizon-server-files",
"horizon-shader-api",
@@ -1374,6 +1374,62 @@ dependencies = [
"winit",
]
[[package]]
name = "horizon-client-lib"
version = "0.1.0"
dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"flate2",
"fs4",
"gilrs",
"glam",
"horizon-anticheat",
"horizon-assets",
"horizon-bench",
"horizon-chat-filter",
"horizon-client-core",
"horizon-client-input",
"horizon-client-render",
"horizon-command",
"horizon-core",
"horizon-dualsense",
"horizon-friends",
"horizon-hdr",
"horizon-items",
"horizon-mods",
"horizon-narrator",
"horizon-optifine",
"horizon-protocol",
"horizon-server-files",
"horizon-shader-api",
"horizon-shaders",
"horizon-telemetry",
"horizon-theme",
"horizon-threads",
"horizon-world",
"log",
"miniz_oxide 0.8.9",
"naga",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"open",
"png",
"pollster",
"rustix 1.1.5",
"serde",
"serde_json",
"sha1",
"sha2",
"unicode-bidi",
"ureq",
"wgpu",
"winit",
"zip",
]
[[package]]
name = "horizon-client-render"
version = "0.1.0"
+1
View File
@@ -8,6 +8,7 @@ members = [
"crates/horizon-hdr",
"crates/horizon-world",
"crates/horizon-client",
"crates/horizon-client-lib",
"crates/horizon-client-core",
"crates/horizon-client-input",
"crates/horizon-client-render",
+221
View File
@@ -0,0 +1,221 @@
[package]
name = "horizon-client-lib"
autobins = false
autoexamples = false
autotests = false
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "Reusable client systems, control tool and parity probes"
[lib]
path = "../horizon-client/src/client_lib.rs"
[dependencies]
horizon-assets = { path = "../horizon-assets" }
horizon-client-core = { path = "../horizon-client-core" }
horizon-client-input = { path = "../horizon-client-input" }
horizon-client-render = { path = "../horizon-client-render" }
horizon-command = { path = "../horizon-command" }
horizon-items = { path = "../horizon-items" }
horizon-core = { path = "../horizon-core" }
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
horizon-optifine = { path = "../horizon-optifine" }
horizon-threads = { path = "../horizon-threads" }
horizon-hdr = { path = "../horizon-hdr" }
horizon-world = { path = "../horizon-world" }
horizon-protocol = { path = "../horizon-protocol" }
# server.properties, eula.txt and the user lists, shared with horizon-server.
horizon-server-files = { path = "../horizon-server-files" }
# The dedicated server's anticheat, off unless server.toml's [anticheat] turns it on (docs/anticheat.md).
horizon-anticheat = { path = "../horizon-anticheat" }
# The dedicated server's chat filter: [chat_filter], the slur list, mutes (docs/chat-filter.md).
horizon-chat-filter = { path = "../horizon-chat-filter" }
# Shader packs: reading them and turning their GLSL into naga modules.
horizon-shaders = { path = "../horizon-shaders" }
# Horizon's own shader packs: the format, the stages and the check (docs/shaders.md).
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-friends = { path = "../horizon-friends" }
# The narrator (GameNarrator): text-to-speech of chat, system messages and screens.
horizon-narrator = { path = "../horizon-narrator" }
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
# Their runtime is the `mods` feature.
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl", "naga-ir"] }
winit = "0.30.13"
pollster = "0.4"
bytemuck = { version = "1.24", features = ["derive"] }
glam = "0.30"
png = "0.18"
sha2 = "0.10.9"
# Server resource packs are checked and cached by their SHA-1, as vanilla does.
sha1 = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
# hlsl-out: the gfx.rs tests translate every shader as wgpu's DirectX 12 backend does. A normal feature,
# not a dev-dependency's, so builds, tests and examples share one build of naga, wgpu and this crate.
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
miniz_oxide = "0.8"
ab_glyph = "0.2.32"
log = "0.4"
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
horizon-theme = { path = "../horizon-theme" }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md); `live` is
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-bench = { path = "../horizon-bench" }
zip = { version = "9.1.3", default-features = false, features = ["deflate"] }
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input); the `controllers` feature.
gilrs = { version = "0.11", optional = true }
# The DualSense's rumble, adaptive triggers and lights over HID (gilrs only reads it).
horizon-dualsense = { path = "../horizon-dualsense", optional = true }
# HTTPS to the Minecraft services (online-mode session join).
ureq = { version = "2", default-features = false, features = ["tls"] }
unicode-bidi = "0.3.18"
# `LevelStorageAccess.estimateDiskSpace`: usable disk space, without unsafe code on Windows.
fs4 = { version = "1.1.0", default-features = false, features = ["sync"] }
[target.'cfg(unix)'.dependencies]
# The server thread's CPU time per tick (`HORIZON_SPIKES`, spans.rs).
rustix = { version = "1", default-features = false, features = ["std", "time", "process", "fs"] }
[target.'cfg(windows)'.dependencies]
# Use the registered URI handler without routing links through Explorer/cmd.
open = { version = "5.4.5", features = ["shellexecute-on-windows"] }
# DirectX 12, where Vulkan has no adapter or device, or by choice (`graphicsApi`, graphics.rs).
# Shaders compile with FXC (d3dcompiler_47.dll, part of Windows), or DXC when dxcompiler.dll is
# on the PATH.
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
[target.'cfg(target_os = "macos")'.dependencies]
# Splitscreen: a window filling a monitor goes over its menu bar (game/split/windows.rs
# `above_menu_bar`); all already in the tree through winit and wgpu.
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow", "NSScreen"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSArray", "NSString"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
# OpenGL (WGL on Windows, EGL on Linux), the last fallback, for GPUs with no Vulkan or
# DirectX 12 driver (`graphicsApi`, graphics.rs).
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[features]
default = ["audio", "controllers"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
audio = ["horizon-client-render/audio"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense", "horizon-client-input/controllers"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
# Compatibility with earlier experiment commands: main already uses mimalloc.
windows-mimalloc = []
# Opt-in Windows heap experiment; see docs/windows-allocators.md.
windows-segment-heap = []
[lints]
workspace = true
[dev-dependencies]
horizon-client-render = { path = "../horizon-client-render", features = ["test-support"] }
horizon-items = { path = "../horizon-items", features = ["test-support"] }
# Every WGSL shader translated as wgpu's DirectX 12 backend does (gfx.rs tests).
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out", "msl-out", "spv-out"] }
[[bin]]
name = "horizon-ctl"
path = "../horizon-client/src/bin/horizon-ctl/main.rs"
[[example]]
name = "chunk_load_bench"
path = "../horizon-client/examples/chunk_load_bench.rs"
[[example]]
name = "gameplay_decode_profile"
path = "../horizon-client/examples/gameplay_decode_profile.rs"
[[example]]
name = "json_load_bench"
path = "../horizon-client/examples/json_load_bench.rs"
[[example]]
name = "mod_port_probe"
path = "../horizon-client/examples/mod_port_probe.rs"
[[example]]
name = "model_library_bench"
path = "../horizon-client/examples/model_library_bench.rs"
[[example]]
name = "model_states_bench"
path = "../horizon-client/examples/model_states_bench.rs"
[[example]]
name = "old_world_fixture"
path = "../horizon-client/examples/old_world_fixture.rs"
[[example]]
name = "portal_scan_bench"
path = "../horizon-client/examples/portal_scan_bench.rs"
[[example]]
name = "random_tick_eligibility_bench"
path = "../horizon-client/examples/random_tick_eligibility_bench.rs"
[[example]]
name = "resource_memory"
path = "../horizon-client/examples/resource_memory.rs"
[[example]]
name = "save_batch_bench"
path = "../horizon-client/examples/save_batch_bench.rs"
[[example]]
name = "shared_json_startup_profile"
path = "../horizon-client/examples/shared_json_startup_profile.rs"
[[example]]
name = "sprite_cache_parity"
path = "../horizon-client/examples/sprite_cache_parity.rs"
[[example]]
name = "client_probe"
path = "../horizon-client/examples/probe/main.rs"
[[test]]
name = "custom_component_api"
path = "../horizon-client/tests/custom_component_api.rs"
[[test]]
name = "custom_component_unregistered"
path = "../horizon-client/tests/custom_component_unregistered.rs"
[[test]]
name = "custom_component_wire"
path = "../horizon-client/tests/custom_component_wire.rs"
[[test]]
name = "forced_chunks"
path = "../horizon-client/tests/forced_chunks.rs"
[[test]]
name = "generation_saves"
path = "../horizon-client/tests/generation_saves.rs"
[[test]]
name = "lan_bot"
path = "../horizon-client/tests/lan_bot.rs"
[[test]]
name = "streaming_meshes"
path = "../horizon-client/tests/streaming_meshes.rs"
[[test]]
name = "tall_column"
path = "../horizon-client/tests/tall_column.rs"
[[test]]
name = "wide_states"
path = "../horizon-client/tests/wide_states.rs"
+14 -8
View File
@@ -6,8 +6,12 @@ edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "Horizon native development client"
autobins = false
autoexamples = false
autotests = false
[dependencies]
horizon-client-lib = { path = "../horizon-client-lib", default-features = false }
horizon-assets = { path = "../horizon-assets" }
horizon-client-core = { path = "../horizon-client-core" }
horizon-client-input = { path = "../horizon-client-input" }
@@ -16,7 +20,6 @@ horizon-command = { path = "../horizon-command" }
horizon-items = { path = "../horizon-items" }
horizon-core = { path = "../horizon-core" }
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
horizon-optifine = { path = "../horizon-optifine" }
horizon-threads = { path = "../horizon-threads" }
horizon-hdr = { path = "../horizon-hdr" }
horizon-world = { path = "../horizon-world" }
@@ -104,20 +107,23 @@ winresource = "0.1"
default = ["audio", "controllers"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
audio = ["horizon-client-render/audio"]
audio = ["horizon-client-render/audio", "horizon-client-lib/audio"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense", "horizon-client-input/controllers"]
controllers = ["horizon-client-lib/controllers", "dep:gilrs", "dep:horizon-dualsense", "horizon-client-input/controllers"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
rng-trace = ["horizon-core/rng-trace", "horizon-client-lib/rng-trace"]
# Compatibility with earlier experiment commands: main already uses mimalloc.
windows-mimalloc = []
# Opt-in Windows heap experiment; see docs/windows-allocators.md.
windows-segment-heap = []
# Its own name, not `probe`: Cargo copies every crate's examples into one target/<profile>/examples.
[[example]]
name = "client_probe"
path = "examples/probe/main.rs"
[[bin]]
name = "horizon-client"
path = "src/main.rs"
[[test]]
name = "launch_api"
path = "tests/launch_api.rs"
[lints]
workspace = true
@@ -12,6 +12,7 @@
//! this thread's CPU time in `Mesher::mesh`, the best of `--rounds`, so other
//! work on the machine changes it less. The checksum of the meshes shows that an
//! A/B pair of builds made the same ones.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
assets::Assets,
biome::Biomes,
@@ -1,6 +1,7 @@
//! Profile the two mandatory passes over gameplay-facts.json independently.
//! Build with `cargo build --release -p horizon-client --example gameplay_decode_profile`.
//! Each invocation runs one phase in a separate process for CPU/RSS measurements.
extern crate horizon_client_lib as horizon_client;
#[allow(dead_code)]
#[path = "../../horizon-items/src/blocks/decode.rs"]
mod decode;
@@ -1,5 +1,6 @@
//! Compare the read + Value parse stages in BlockFacts::load and Assets::load.
//! Usage: json_load_bench RESOURCE_ROOT [REPETITIONS]; see tools/json_load_fixtures.py.
extern crate horizon_client_lib as horizon_client;
use serde_json::Value;
use std::{
fs::{self, File},
@@ -7,6 +7,7 @@
//!
//! WORK is a `horizon-port-pack` work folder: `oracle/facts.json` and
//! `mods/<id>/carry/`. The root is built in `WORK/horizon-root`.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
Result,
assets::Assets,
@@ -2,6 +2,7 @@
//! for live requested heap bytes; leave it unset for latency measurements.
//! cargo run --profile fast -p horizon-client --example model_library_bench -- ROOT [--parity]
//! Run fresh processes, warm up twice, then compare multiple alternating baseline/candidate runs.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{assets::Assets, item_render::ItemRenderer};
use std::{
alloc::{GlobalAlloc, Layout, System},
@@ -1,5 +1,6 @@
//! Compare the old two-parse loading path with catalog construction from shared facts.
//! Run: cargo run -p horizon-client --profile fast --example model_states_bench -- 15
extern crate horizon_client_lib as horizon_client;
use horizon_client::{Result, models::StateModels, sprites::Sprites};
use horizon_core::position::BlockPos;
use horizon_world::{
@@ -5,6 +5,7 @@
//! generated around them blend with them (`Blender.of`).
//!
//! old_world_fixture <source world> <destination world> <x0,z0,x1,z1>
extern crate horizon_client_lib as horizon_client;
use horizon_core::position::ChunkPos;
use horizon_world::{
nbt::{Limits, Tag},
@@ -2,6 +2,7 @@
//! entity reaches, with the per-tick palette map against the resident cache.
//! Run with prepared client resources:
//! cargo run --profile fast -p horizon-client --example portal_scan_bench -- RESOURCES
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS, Section},
@@ -4,6 +4,7 @@
//!
//! This file only lists the probes, so that adding one reruns no harness: tools/parity.py leaves it out of
//! their dependencies. How a probe runs is in dispatch.rs.
extern crate horizon_client_lib as horizon_client;
#[macro_use]
mod dispatch;
@@ -1,5 +1,6 @@
//! Selection cost only (not block simulation). Run with prepared client resources.
//! cargo run --profile fast -p horizon-client --example random_tick_eligibility_bench -- RESOURCES
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, Section},
@@ -9,6 +9,7 @@
//! and the process RSS (what the allocator holds on to). On macOS it ends
//! with `footprint`'s physical footprint, the number the OS charges: RSS also
//! counts freed pages the allocator marked reusable.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
assets::Assets, biome::Biomes, blocks::BlockFacts, collisions, crafting::Recipes, items::Items,
light::LightTables, mesher::MeshTables, models, motion::MotionResources,
@@ -2,6 +2,7 @@
//! full chunk NBT encoder. Run with `cargo run --profile fast -p horizon-client
//! --example save_batch_bench`. Add `-- --nbt` to also time serialization.
//! Exact NBT equality is always validated. No GPU, compression or disk I/O timed.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
biome::Biomes,
chunk::{Chunk, block_strategy},
@@ -2,6 +2,7 @@
//! Run each mode in a separate process to capture CPU usage and peak RSS.
//! The deferred mode evaluates both phases contiguously for fair loader comparison;
//! actual client startup performs other initialization between these phases.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{assets::Assets, blocks::BlockFacts};
use horizon_world::level::Behaviours;
use std::{path::Path, time::Instant};
@@ -2,6 +2,7 @@
//! to the ones decoded from the PNGs. Deletes the cache file in ROOT, loads (decodes
//! and writes it), loads again (reads it) and compares the sprite arrays.
//! cargo run --profile fast -p horizon-client --example sprite_cache_parity -- ROOT
extern crate horizon_client_lib as horizon_client;
use horizon_client::assets::{Assets, RenderAssets};
use std::{
hash::{DefaultHasher, Hash, Hasher},
@@ -1,6 +1,7 @@
//! horizon-ctl: play and script a running Horizon client through its control
//! socket, from a shell, a script or an AI agent (`horizon-ctl mcp`).
#![forbid(unsafe_code)]
extern crate horizon_client_lib as horizon_client;
mod commands;
mod launch;
mod mcp;
+160
View File
@@ -0,0 +1,160 @@
//! Horizon native client: resources, world streaming, rendering and the
//! bindings between the validated simulation pieces and the game loop.
#![forbid(unsafe_code)]
extern crate self as horizon_client;
pub mod advancements;
pub mod ambience;
pub mod animate_tick;
pub mod arrows;
pub mod block_contact;
pub use horizon_client_render::{
assets, atlas_sources, audio, auth, ban_notice, bidi, biome, block_entities, block_entity_anim,
block_entity_render, border_render, breaking, brewing, camera, chat, chunk, chunk_map, clouds,
collisions, command_block, containers, crash, datapack, dynamic_light, effects, environment,
extras, fast_hash, fog, font, gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud,
hud_layout, hud_overlays, inventory_effects, java_text, jigsaw_block, level_loading, light,
lod, logging, menu_blur, mesher, minimap, mipmap, mod_list, model_library, models,
multiline_edit, native_emissive, options_file, options_fixes, options_ui, overlay, pack_screen,
packs, par, particles, poi_sections, post_chain, potent_sulfur, preedit, providers,
screen_draw, screen_effects, sculk, section_mask, settings, shader_options, shader_pack_list,
signs, skins, sound_engine, sound_events, sound_preview, spans, split_layout, split_profiles,
sprites, streaming, structure_block, tab_list, telemetry, test_block, test_instance_block,
testgen, title_edition, trims, ui, vault, weather, widgets, world, worldborder, zip,
};
pub mod attack_strength;
pub mod attribute_updates;
pub mod auto_jump;
pub mod banner;
pub mod beehive;
pub mod benchmark;
pub mod block_entity_item;
pub mod block_pattern;
pub mod block_transform;
pub mod boss_overlay;
pub mod cauldron;
pub mod chunk_io;
pub mod clock;
pub mod command_storage;
pub mod confirm_screen;
pub mod container_screen;
pub mod content_identity;
pub mod control;
pub mod crafting;
pub mod creative;
pub mod cushion;
pub mod data_cmd;
pub mod debug_chart;
pub mod debug_entries;
pub mod debug_keys;
pub mod debug_overlay;
pub mod debugworld;
pub mod dialog;
pub mod dialog_screen;
pub mod discord_presence;
pub mod end_crystal;
pub mod end_gateway;
pub mod entities;
pub mod entity_culling;
pub mod entity_nbt;
pub mod extras_hud;
pub mod falling_block;
pub mod fire_render;
pub mod first_person;
pub mod flatgen;
pub mod friends_screen;
pub mod furnace;
pub mod game_mode_switcher;
pub mod game_rules_screen;
pub mod gizmos;
pub mod gossip;
pub mod health;
pub mod heartbeat;
pub mod hotbar;
pub mod hunger;
pub mod inv_sort;
pub mod item_activation;
pub mod item_render;
pub mod item_sync;
pub mod item_tooltip;
pub mod lan;
pub mod landing;
pub mod locator_bar;
pub mod lod_net;
pub mod menu;
pub mod merchant;
pub mod mobs;
pub mod mod_content;
pub mod motion;
pub mod multiplayer;
pub mod music;
pub mod native_data;
pub mod net;
pub mod nether_portal;
pub mod no_resources;
pub mod offline_name;
pub mod old_world;
pub mod osk;
pub mod patrol;
pub mod perf_overlay;
pub mod pick_item;
pub mod player_sync;
pub mod portal_sections;
pub mod pose;
pub mod pregen;
pub mod projectiles;
pub mod raid;
pub mod random_tick_sections;
pub mod recipe_book;
pub mod recipe_book_screen;
pub mod reconnect;
pub mod save;
pub mod scoreboard_cmd;
pub mod screen_layout;
pub mod screen_narrator;
pub use horizon_server_files as server;
pub mod server_packs;
pub mod shadows;
pub mod spear;
pub mod spectator_menu;
pub mod sprint;
pub mod stations;
pub mod stopwatches;
pub mod structure_blocks;
pub mod text_display;
pub mod tick_chunk_membership;
pub mod ticket_levels;
pub mod title;
pub mod toasts;
pub mod ui_theme;
pub mod vanillagen;
pub mod win_screen;
pub mod world_spawn;
pub use horizon_client_core::Result;
pub use horizon_client_core::block_entity_nbt;
pub use horizon_client_core::bossbar;
pub use horizon_client_core::component;
pub use horizon_client_core::fastutil_order;
pub use horizon_client_core::gamerules;
pub use horizon_client_core::lang;
pub use horizon_client_core::nbt_path;
pub use horizon_client_core::scoreboard;
pub use horizon_client_core::snbt;
pub use horizon_client_core::text;
pub use horizon_client_input::clipboard;
pub use horizon_client_input::gamepad;
pub use horizon_client_input::input;
pub use horizon_client_input::interaction_input;
pub use horizon_client_input::key_names;
pub use horizon_client_input::keybinds;
pub use horizon_client_input::keymapping;
pub use horizon_client_input::mouse;
pub use horizon_client_input::window;
pub use horizon_command as command;
pub use horizon_items::blocks;
pub use horizon_items::bundle;
pub use horizon_items::cooldowns;
pub use horizon_items::enchant;
pub use horizon_items::flowers;
pub use horizon_items::item_data;
pub use horizon_items::items;
pub use horizon_items::random_sequences;
+107
View File
@@ -0,0 +1,107 @@
//! Startup shared by the gameplay library and its executable.
use crate::{crash, game, logging};
/// Start the game. The executable supplies the allocator and retains the mod API marker.
pub fn main() {
horizon_threads::configure_allocator();
horizon_client::telemetry::mark_start();
let args: Vec<String> = std::env::args().skip(1).collect();
let game_args = args.iter().take_while(|a| *a != "--dedicated");
let help = game_args
.clone()
.any(|a| a == "--help" || a == "-h" || a == "--launch-api");
// --home before the log opens in it.
if let Some(dir) = game_args.clone().skip_while(|a| *a != "--home").nth(1) {
logging::set_game_home(std::path::absolute(dir).unwrap_or_else(|_| dir.into()));
}
// The dedicated server logs in its own folder (--game-dir's), as vanilla's does; the game in its home.
let (home, side, thread) = if args.iter().any(|a| a == "--dedicated") {
let folder = game_args
.clone()
.skip_while(|a| *a != "--game-dir" && *a != "--gameDir")
.nth(1)
.and_then(|dir| std::path::absolute(dir).ok())
.unwrap_or_else(|| ".".into());
(folder, "server", "Server thread")
} else {
// --player-dir too (a game's, not a server's): a player of a shared game folder keeps
// its log and crash reports in its own; crash telemetry stays with the home's consent.
if let Some(dir) = game_args
.clone()
.skip_while(|a| *a != "--player-dir")
.nth(1)
{
logging::set_player_dir(std::path::absolute(dir).unwrap_or_else(|_| dir.into()));
}
(logging::game_home(), "client", "Render thread")
};
if !help {
logging::init(
logging::player_dir_override().unwrap_or(home.as_path()),
thread,
);
// The log's first line names the build, so a bug report that carries the log says which one it was.
log::info!("Horizon {}", crash::version());
crash::install(home.clone(), side);
}
raise_open_file_limit();
if let Err(error) = game::run() {
// A bad command line is the caller's mistake, not a crash: no report.
let usage = error.is::<game::UsageError>();
log::error!("Horizon: {error}");
logging::flush();
// The report's path is the last line on stderr (crash::SAVED_LINE).
if !help && !usage {
crash::report_error(&home, side, &*error);
}
// Started from Explorer, the console window closes with the game:
// keep the message on screen until it is read.
#[cfg(windows)]
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
eprintln!("Press Enter to close.");
let _ = std::io::stdin().read_line(&mut String::new());
}
std::process::exit(if usage {
game::exit_code::USAGE
} else {
game::exit_code::ERROR
});
}
logging::flush();
}
/// The JVM raises its open-file limit to the hard limit as it starts
/// (`-XX:+MaxFDLimit`, on by default), so a vanilla server may hold as many
/// sockets and region files as the host allows, not the soft default (often
/// 1024 in a container). The same here; macOS caps it at `OPEN_MAX`, as the
/// JVM does there.
fn raise_open_file_limit() {
#[cfg(unix)]
{
use rustix::process::{Resource, Rlimit, getrlimit, setrlimit};
let limit = getrlimit(Resource::Nofile);
let hard = if cfg!(target_os = "macos") {
Some(limit.maximum.map_or(10_240, |h| h.min(10_240)))
} else {
limit.maximum
};
let raised = match (limit.current, hard) {
(Some(soft), Some(hard)) => hard > soft,
(Some(_), None) => true,
(None, _) => false,
};
if raised {
let to = Rlimit {
current: hard,
maximum: limit.maximum,
};
match setrlimit(Resource::Nofile, to) {
Ok(()) => log::info!(
"Open file limit raised from {:?} to {hard:?}",
limit.current
),
Err(e) => log::warn!("Open file limit stays at {:?}: {e}", limit.current),
}
}
}
}
@@ -5001,7 +5001,7 @@ mod tests {
#[test]
fn loot_table_load_stays_hookable() {
let source = include_str!("../../../horizon-world/src/loot.rs");
let source = include_str!("../../../horizon-world/src/loot.rs").replace("\r\n", "\n");
// From its doc comment's last line, so an attribute above it counts.
let hook = source.find("pub fn native_loot_table_load(").unwrap();
let start = source[..hook].rfind("///").unwrap();
+5 -158
View File
@@ -1,159 +1,6 @@
//! Horizon native client: resources, world streaming, rendering and the
//! bindings between the validated simulation pieces and the game loop.
//! Gameplay and startup, above the reusable client library.
#![forbid(unsafe_code)]
pub mod advancements;
pub mod ambience;
pub mod animate_tick;
pub mod arrows;
pub mod block_contact;
pub use horizon_client_render::{
assets, atlas_sources, audio, auth, ban_notice, bidi, biome, block_entities, block_entity_anim,
block_entity_render, border_render, breaking, brewing, camera, chat, chunk, chunk_map, clouds,
collisions, command_block, containers, crash, datapack, dynamic_light, effects, environment,
extras, fast_hash, fog, font, gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud,
hud_layout, hud_overlays, inventory_effects, java_text, jigsaw_block, level_loading, light,
lod, logging, menu_blur, mesher, minimap, mipmap, mod_list, model_library, models,
multiline_edit, native_emissive, options_file, options_fixes, options_ui, overlay, pack_screen,
packs, par, particles, poi_sections, post_chain, potent_sulfur, preedit, providers,
screen_draw, screen_effects, sculk, section_mask, settings, shader_options, shader_pack_list,
signs, skins, sound_engine, sound_events, sound_preview, spans, split_layout, split_profiles,
sprites, streaming, structure_block, tab_list, telemetry, test_block, test_instance_block,
testgen, title_edition, trims, ui, vault, weather, widgets, world, worldborder, zip,
};
pub mod attack_strength;
pub mod attribute_updates;
pub mod auto_jump;
pub mod banner;
pub mod beehive;
pub mod benchmark;
pub mod block_entity_item;
pub mod block_pattern;
pub mod block_transform;
pub mod boss_overlay;
pub mod cauldron;
pub mod chunk_io;
pub mod clock;
pub mod command_storage;
pub mod confirm_screen;
pub mod container_screen;
pub mod content_identity;
pub mod control;
pub mod crafting;
pub mod creative;
pub mod cushion;
pub mod data_cmd;
pub mod debug_chart;
pub mod debug_entries;
pub mod debug_keys;
pub mod debug_overlay;
pub mod debugworld;
pub mod dialog;
pub mod dialog_screen;
pub mod discord_presence;
pub mod end_crystal;
pub mod end_gateway;
pub mod entities;
pub mod entity_culling;
pub mod entity_nbt;
pub mod extras_hud;
pub mod falling_block;
pub mod fire_render;
pub mod first_person;
pub mod flatgen;
pub mod friends_screen;
pub mod furnace;
pub mod game_mode_switcher;
pub mod game_rules_screen;
pub mod gizmos;
pub mod gossip;
pub mod health;
pub mod heartbeat;
pub mod hotbar;
pub mod hunger;
pub mod inv_sort;
pub mod item_activation;
pub mod item_render;
pub mod item_sync;
pub mod item_tooltip;
pub mod lan;
pub mod landing;
pub mod locator_bar;
pub mod lod_net;
pub mod menu;
pub mod merchant;
pub mod mobs;
pub mod mod_content;
pub mod motion;
pub mod multiplayer;
pub mod music;
pub mod native_data;
pub mod net;
pub mod nether_portal;
pub mod no_resources;
pub mod offline_name;
pub mod old_world;
pub mod osk;
pub mod patrol;
pub mod perf_overlay;
pub mod pick_item;
pub mod player_sync;
pub mod portal_sections;
pub mod pose;
pub mod pregen;
pub mod projectiles;
pub mod raid;
pub mod random_tick_sections;
pub mod recipe_book;
pub mod recipe_book_screen;
pub mod reconnect;
pub mod save;
pub mod scoreboard_cmd;
pub mod screen_layout;
pub mod screen_narrator;
pub use horizon_server_files as server;
pub mod server_packs;
pub mod shadows;
pub mod spear;
pub mod spectator_menu;
pub mod sprint;
pub mod stations;
pub mod stopwatches;
pub mod structure_blocks;
pub mod text_display;
pub mod tick_chunk_membership;
pub mod ticket_levels;
pub mod title;
pub mod toasts;
pub mod ui_theme;
pub mod vanillagen;
pub mod win_screen;
pub mod world_spawn;
pub use horizon_client_core::Result;
pub use horizon_client_core::block_entity_nbt;
pub use horizon_client_core::bossbar;
pub use horizon_client_core::component;
pub use horizon_client_core::fastutil_order;
pub use horizon_client_core::gamerules;
pub use horizon_client_core::lang;
pub use horizon_client_core::nbt_path;
pub use horizon_client_core::scoreboard;
pub use horizon_client_core::snbt;
pub use horizon_client_core::text;
pub use horizon_client_input::clipboard;
pub use horizon_client_input::gamepad;
pub use horizon_client_input::input;
pub use horizon_client_input::interaction_input;
pub use horizon_client_input::key_names;
pub use horizon_client_input::keybinds;
pub use horizon_client_input::keymapping;
pub use horizon_client_input::mouse;
pub use horizon_client_input::window;
pub use horizon_command as command;
pub use horizon_items::blocks;
pub use horizon_items::bundle;
pub use horizon_items::cooldowns;
pub use horizon_items::enchant;
pub use horizon_items::flowers;
pub use horizon_items::item_data;
pub use horizon_items::items;
pub use horizon_items::random_sequences;
extern crate self as horizon_client;
pub use horizon_client_lib::*;
pub mod entry;
pub mod game;
+1 -104
View File
@@ -1,8 +1,5 @@
//! Horizon: native Rust client for Minecraft Java Edition 26.3 gameplay.
#![forbid(unsafe_code)]
mod game;
use horizon_client::{crash, logging};
#[global_allocator]
static ALLOCATOR: horizon_threads::Allocator = horizon_threads::Allocator;
@@ -13,106 +10,6 @@ static ALLOCATOR: horizon_threads::Allocator = horizon_threads::Allocator;
static MOD_API_LEVEL: [u8; 27] = horizon_mods::api_level::MARK;
fn main() {
horizon_threads::configure_allocator();
std::hint::black_box(&MOD_API_LEVEL);
horizon_client::telemetry::mark_start();
let args: Vec<String> = std::env::args().skip(1).collect();
let game_args = args.iter().take_while(|a| *a != "--dedicated");
let help = game_args
.clone()
.any(|a| a == "--help" || a == "-h" || a == "--launch-api");
// --home before the log opens in it.
if let Some(dir) = game_args.clone().skip_while(|a| *a != "--home").nth(1) {
logging::set_game_home(std::path::absolute(dir).unwrap_or_else(|_| dir.into()));
}
// The dedicated server logs in its own folder (--game-dir's), as vanilla's does; the game in its home.
let (home, side, thread) = if args.iter().any(|a| a == "--dedicated") {
let folder = game_args
.clone()
.skip_while(|a| *a != "--game-dir" && *a != "--gameDir")
.nth(1)
.and_then(|dir| std::path::absolute(dir).ok())
.unwrap_or_else(|| ".".into());
(folder, "server", "Server thread")
} else {
// --player-dir too (a game's, not a server's): a player of a shared game folder keeps
// its log and crash reports in its own; crash telemetry stays with the home's consent.
if let Some(dir) = game_args
.clone()
.skip_while(|a| *a != "--player-dir")
.nth(1)
{
logging::set_player_dir(std::path::absolute(dir).unwrap_or_else(|_| dir.into()));
}
(logging::game_home(), "client", "Render thread")
};
if !help {
logging::init(
logging::player_dir_override().unwrap_or(home.as_path()),
thread,
);
// The log's first line names the build, so a bug report that carries the log says which one it was.
log::info!("Horizon {}", crash::version());
crash::install(home.clone(), side);
}
raise_open_file_limit();
if let Err(error) = game::run() {
// A bad command line is the caller's mistake, not a crash: no report.
let usage = error.is::<game::UsageError>();
log::error!("Horizon: {error}");
logging::flush();
// The report's path is the last line on stderr (crash::SAVED_LINE).
if !help && !usage {
crash::report_error(&home, side, &*error);
}
// Started from Explorer, the console window closes with the game:
// keep the message on screen until it is read.
#[cfg(windows)]
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
eprintln!("Press Enter to close.");
let _ = std::io::stdin().read_line(&mut String::new());
}
std::process::exit(if usage {
game::exit_code::USAGE
} else {
game::exit_code::ERROR
});
}
logging::flush();
}
/// The JVM raises its open-file limit to the hard limit as it starts
/// (`-XX:+MaxFDLimit`, on by default), so a vanilla server may hold as many
/// sockets and region files as the host allows, not the soft default (often
/// 1024 in a container). The same here; macOS caps it at `OPEN_MAX`, as the
/// JVM does there.
fn raise_open_file_limit() {
#[cfg(unix)]
{
use rustix::process::{Resource, Rlimit, getrlimit, setrlimit};
let limit = getrlimit(Resource::Nofile);
let hard = if cfg!(target_os = "macos") {
Some(limit.maximum.map_or(10_240, |h| h.min(10_240)))
} else {
limit.maximum
};
let raised = match (limit.current, hard) {
(Some(soft), Some(hard)) => hard > soft,
(Some(_), None) => true,
(None, _) => false,
};
if raised {
let to = Rlimit {
current: hard,
maximum: limit.maximum,
};
match setrlimit(Resource::Nofile, to) {
Ok(()) => log::info!(
"Open file limit raised from {:?} to {hard:?}",
limit.current
),
Err(e) => log::warn!("Open file limit stays at {:?}: {e}", limit.current),
}
}
}
horizon_client::entry::main();
}
@@ -1,4 +1,5 @@
//! Native component registration, value limits and content identity.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
content_identity,
mod_content::{ContentMod, accept_mod, build, known_components},
@@ -1,3 +1,4 @@
extern crate horizon_client_lib as horizon_client;
use horizon_protocol::{
buf::{Codec, Reader, Writer},
item::Slot,
@@ -1,3 +1,4 @@
extern crate horizon_client_lib as horizon_client;
use horizon_client::{item_sync, items::ItemStack, lan::guest::stack_to_network};
use horizon_items::item_nbt::{item_from_nbt, item_to_nbt};
use horizon_protocol::{
@@ -4,6 +4,7 @@
//! column's status (`ChunkLevel.generationStatus`). The chunk itself and the
//! two rings around it are level chunks with their light, the third ring has
//! its features and the fourth only its terrain.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
assets::Assets,
biome::Biomes,
@@ -3,6 +3,7 @@
//! after its own decoration and its eight neighbours', and chunks that
//! decoration wrote to are saved rather than generated again without those
//! writes. Bastions lost whole chunks of pieces this way.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
assets::Assets,
biome::Biomes,
+1
View File
@@ -2,6 +2,7 @@
//! handshake, status, offline login, configuration (registries and tags),
//! then the play state (login, chunks with light, player list, entities,
//! block changes). Needs the prepared resources; skipped without them.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
chunk::{Chunk, MIN_Y},
lan::{
@@ -4,6 +4,7 @@
//! came closer, so they stayed invisible until a block in them changed.
//! Relit columns remesh only where their light changed (#270), and every
//! mesh still matches the world.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
assets::Assets,
biome::Biomes,
@@ -1,6 +1,7 @@
//! A level taller than vanilla's (To The Max's Overworld: -64..2032, 131
//! sections) streams, lights and meshes to its top, and edits near the top
//! relight and remesh it.
extern crate horizon_client_lib as horizon_client;
use horizon_client::{
assets::Assets,
biome::Biomes,
@@ -2,6 +2,7 @@
//! content mods can take the registry past 65,536 states. Its own test binary,
//! since the palette width is set once for the whole process.
extern crate horizon_client_lib as horizon_client;
use horizon_client::chunk::{Chunk, Section, block_strategy, set_block_states};
#[test]
+34 -1
View File
@@ -306,6 +306,15 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
@@ -319,7 +328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.9.1",
"zlib-rs",
]
@@ -506,6 +515,7 @@ version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
"png",
"serde",
"serde_json",
"sha2",
@@ -783,6 +793,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.9.1"
@@ -911,6 +931,19 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]]
name = "potential_utf"
version = "0.1.6"
+18 -3
View File
@@ -41,9 +41,10 @@ pub struct Index {
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum Kind {
/// A `.hmod`.
/// A `.hmod` of content only.
Mod,
/// A source-only native mixin bundle, compiled into an isolated game build.
/// A source-only native mixin bundle, compiled into an isolated game build. Published as an
/// `.hnmod` until launchers and the game read native `.hmod`s (#5865, docs/api/mod-files.md).
NativeMod,
/// A `.hpack`: registry mods played together as a profile.
Modpack,
@@ -557,8 +558,22 @@ impl Facts {
/// this build doesn't know are refused: their checks come with their formats.
pub fn inspect(kind: &Kind, path: &Path) -> Result<Facts, String> {
match kind {
Kind::NativeMod => Ok(native::Bundle::read(path)?.manifest.facts()),
Kind::NativeMod => {
let facts = native::Bundle::read(path)?.manifest.facts();
// A `.hmod` with content too: checked as the game reads it.
if Package::has_manifest(path) {
let m = Package::read(path)?.manifest;
crate::Content::read(path, &m)?;
}
Ok(facts)
}
Kind::Mod => {
if native::holds(path) {
return Err(format!(
"it has native code ({}): it is a native-mod",
native::MANIFEST
));
}
let Package { manifest: m, wasm } = Package::read(path)?;
crate::Content::read(path, &m)?;
let api_level = match wasm {
+93 -4
View File
@@ -5,6 +5,31 @@ use std::{collections::BTreeMap, io::Read, path::Path};
pub const MANIFEST: &str = "horizon-native.json";
/// Why a native mod may not carry `mod.wasm`: WebAssembly mods don't run any more.
pub const WASM_GONE: &str = "mod.wasm: WebAssembly mods don't run any more, port the code to a native mod (docs/native-mods.md)";
/// Whether `path` names a native mod's file: an `.hnmod`, or a `.hmod` holding native code
/// (one file for content and code, docs/native-mods.md).
pub fn is_file(path: &Path) -> bool {
path.extension()
.is_some_and(|e| e == "hnmod" || e == "hmod")
}
/// Whether the mod file at `path` carries native code ([`MANIFEST`]): every `.hnmod`, and a
/// `.hmod` with code beside or instead of its content.
pub fn holds(path: &Path) -> bool {
std::fs::File::open(path)
.ok()
.and_then(|f| zip::ZipArchive::new(f).ok())
.is_some_and(|mut zip| zip.by_name(MANIFEST).is_ok())
}
/// Whether a `.hmod`'s entry `name` is its content's (`horizon-mod.toml`, `assets/`, `data/`),
/// which [`crate::Content`] reads and checks, not the native build.
fn content(name: &str) -> bool {
name == crate::manifest::MANIFEST || name.starts_with("assets/") || name.starts_with("data/")
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
@@ -67,7 +92,7 @@ impl Manifest {
pub struct Bundle {
pub manifest: Manifest,
/// Validated relative paths: Rust and TOML, plus files under `mixins/<id>/data/` or a
/// `data/` folder deeper in (a crate's).
/// `data/` folder deeper in (a crate's). A `.hmod`'s content files are not among them.
pub files: BTreeMap<String, Vec<u8>>,
}
@@ -102,10 +127,8 @@ impl Bundle {
pub fn read(path: &Path) -> Result<Self, String> {
let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
let mut zip = zip::ZipArchive::new(file).map_err(|e| e.to_string())?;
if zip.len() > 512 {
return Err("native bundle has too many entries".into());
}
let mut files = BTreeMap::new();
let mut content_id = None;
let mut total = 0u64;
for i in 0..zip.len() {
let mut entry = zip.by_index(i).map_err(|e| e.to_string())?;
@@ -125,6 +148,23 @@ impl Bundle {
{
return Err(format!("invalid native bundle path: {name}"));
}
if name == crate::manifest::WASM {
return Err(WASM_GONE.into());
}
if content(&name) {
if name == crate::manifest::MANIFEST {
let mut text = String::new();
(&mut entry)
.take(1 << 20)
.read_to_string(&mut text)
.map_err(|e| format!("{name}: {e}"))?;
content_id = Some(crate::Manifest::parse(&text)?.id);
}
continue;
}
if files.len() >= 512 {
return Err("native bundle has too many entries".into());
}
let mut bytes = Vec::new();
(&mut entry)
.take(super::MAX_FILE + 1)
@@ -142,6 +182,13 @@ impl Bundle {
serde_json::from_slice(files.get(MANIFEST).ok_or("missing horizon-native.json")?)
.map_err(|e| e.to_string())?;
manifest.check()?;
if let Some(id) = content_id.filter(|id| *id != manifest.id) {
return Err(format!(
"its {} is mod {id}, its {MANIFEST} {}: one file is one mod",
crate::manifest::MANIFEST,
manifest.id
));
}
let prefix = format!("mixins/{}/", manifest.id);
for name in files.keys().filter(|n| n.as_str() != MANIFEST) {
let Some(rest) = name.strip_prefix(&prefix) else {
@@ -282,6 +329,48 @@ mod tests {
}
}
#[test]
fn one_hmod_carries_content_beside_its_native_code() {
let toml = r#"id = "veinminer"
name = "Veinminer"
version = "0.3.0"
api = "0.2"
capabilities = ["registries"]
"#;
let bundle = read(
"unified",
&[
("horizon-mod.toml", toml.as_bytes()),
("assets/veinminer/lang/en_us.json", b"{}"),
("data/veinminer/tags/item/ores.json", b"{}"),
("mixins/veinminer/hook.rs", b"fn hook() {}"),
],
&mixin(),
)
.unwrap();
let names: Vec<&str> = bundle.files.keys().map(String::as_str).collect();
assert_eq!(
names,
[
MANIFEST,
"mixins/veinminer/hook.rs",
"mixins/veinminer/horizon-mixin.toml"
]
);
let other = toml.replace(r#""veinminer""#, r#""other""#);
let e = read(
"two-ids",
&[("horizon-mod.toml", other.as_bytes())],
&mixin(),
)
.err()
.unwrap();
assert!(e.contains("one file is one mod"), "{e}");
let e = read("wasm", &[("mod.wasm", b"\0asm")], &mixin())
.err()
.unwrap();
assert_eq!(e, WASM_GONE);
}
#[test]
fn rejects_missing_or_mismatched_mixin_identity_without_panicking() {
assert!(read("missing-mod", &[], "[unrelated]\nvalue = 1").is_err());
assert!(
+9 -9
View File
@@ -354,8 +354,8 @@ pub fn project(entry: &Entry, src: &Sources) -> Result<Project, Vec<String>> {
for v in &entry.versions {
match version(entry, v, src) {
// A mod that turned native keeps its `.hmod` versions published, but the index offers
// only its native ones: a launcher never installs a `.hmod` as a native mod.
Ok(_) if file_kind(entry, v) != entry.kind => {}
// only its native ones: a launcher never installs a content-only `.hmod` as a native mod.
Ok(_) if file_kind(entry, v, &src.files.join(&v.sha256)) != entry.kind => {}
Ok(v) => versions.push(v),
Err(e) => errors.push(format!("{id} {}: {e}", v.version)),
}
@@ -384,11 +384,11 @@ pub fn project(entry: &Entry, src: &Sources) -> Result<Project, Vec<String>> {
})
}
/// The kind of a version's file: the project's, but a `.hmod` of a mod that turned native
/// (`review`) is a WebAssembly mod's.
fn file_kind(entry: &Entry, v: &EntryVersion) -> Kind {
/// The kind of a version's file at `path`: the project's, but a `.hmod` without native code of a
/// mod that turned native (`review`) is from before it did.
fn file_kind(entry: &Entry, v: &EntryVersion, path: &Path) -> Kind {
match entry.kind {
Kind::NativeMod if v.file.ends_with(".hmod") => Kind::Mod,
Kind::NativeMod if v.file.ends_with(".hmod") && !super::native::holds(path) => Kind::Mod,
_ => entry.kind.clone(),
}
}
@@ -425,7 +425,7 @@ fn version_at(
(None, Some(_)) => return Err("signed, but the project has no key".into()),
(None, None) => {}
}
let facts = inspect(&file_kind(entry, v), path)?;
let facts = inspect(&file_kind(entry, v, path), path)?;
if facts.id.as_ref().is_some_and(|id| *id != entry.id) {
return Err(format!(
"the file's id is {:?}, not {:?}",
@@ -598,7 +598,7 @@ pub struct Review {
/// Compares the entries of a pull request (`head`) with `main`'s (`base`):
/// a published file never changes and is never removed (take it down in
/// `revoked.toml`), a project's kind never changes but for a mod turning
/// native (its next versions are `.hnmod`s), and changes by someone who
/// native (its next versions carry native code), and changes by someone who
/// isn't an owner, or to owners and keys, are pointed out.
pub fn review(base: &[Entry], head: &[Entry], author: &str) -> Review {
let mut r = Review::default();
@@ -622,7 +622,7 @@ pub fn review(base: &[Entry], head: &[Entry], author: &str) -> Review {
}
if (&old.kind, &new.kind) == (&Kind::Mod, &Kind::NativeMod) {
r.notes.push(format!(
"{}: turns native; its .hmod versions stay published, and launchers keep the installed copies",
"{}: turns native; its content-only .hmod versions stay published, and launchers keep the installed copies",
old.id
));
} else if new.kind != old.kind {
@@ -259,9 +259,27 @@ fn a_mod_may_turn_native_once() {
);
assert!(r.errors.is_empty(), "{r:?}");
assert!(r.notes.iter().any(|n| n.contains("turns native")), "{r:?}");
// Its .hmod versions are a WebAssembly mod's files; the new ones native.
assert_eq!(file_kind(&native, &native.versions[1]), Kind::Mod);
assert_eq!(file_kind(&native, &native.versions[0]), Kind::NativeMod);
// Its content-only .hmod versions are from before; the new ones native, and a .hmod
// holding native code is native too.
let none = Path::new("no-such-file");
assert_eq!(file_kind(&native, &native.versions[1], none), Kind::Mod);
assert_eq!(
file_kind(&native, &native.versions[0], none),
Kind::NativeMod
);
let unified = std::env::temp_dir().join(format!("unified-{}.hmod", std::process::id()));
let mut zip = zip::ZipWriter::new(std::fs::File::create(&unified).unwrap());
zip.start_file(
super::super::native::MANIFEST,
zip::write::SimpleFileOptions::default(),
)
.unwrap();
zip.finish().unwrap();
assert_eq!(
file_kind(&native, &native.versions[1], &unified),
Kind::NativeMod
);
std::fs::remove_file(unified).unwrap();
// And never back.
let r = review(&[native], &[base], "ludovic111");
assert!(
+11
View File
@@ -362,6 +362,17 @@ impl Package {
}
Ok(Package { manifest, wasm })
}
/// Whether the mod at `path` (a `.hmod` or a folder) has a [`MANIFEST`]: content, or a legacy
/// WebAssembly mod. A `.hmod` holding only native code has none.
pub fn has_manifest(path: &Path) -> bool {
if path.is_dir() {
return path.join(MANIFEST).is_file();
}
std::fs::File::open(path)
.ok()
.and_then(|f| zip::ZipArchive::new(f).ok())
.is_some_and(|mut zip| zip.by_name(MANIFEST).is_ok())
}
/// The `.hmod` bytes for `manifest_text` and a component.
pub fn write(manifest_text: &str, wasm: &[u8]) -> Result<Vec<u8>, String> {
Self::write_files(manifest_text, Some(wasm), &[])
+2 -2
View File
@@ -10,8 +10,8 @@ and worlds. Each entry names its version field, its documentation and its level
| Benchmark report | the benchmark's output, the local log, uploads | `"format": "horizon-benchmark-report"`, `"version": 1` | stable | [benchmark-report.md](../benchmark-report.md) |
| Telemetry batch | `POST /v1/batch` from the launcher | `"schema": 1` | stable | [telemetry.md](../telemetry.md) |
| Telemetry consent | `telemetry/consent.json` in the game folder | `"version": 2` | stable | [telemetry.md](../telemetry.md) |
| Content mod package | `.hmod`: a zip with `horizon-mod.toml`, `assets/`, `data/` | the manifest's `api` | preview | [mods.md](../mods.md#the-manifest), [mod-index.md](mod-index.md) |
| Native mod package | `.hnmod`: a zip with `horizon-native.json` and the bundle's `.rs` and `.toml` files | `schema` | experimental | [native-mods.md](../native-mods.md) |
| Content mod package | `.hmod`: a zip with `horizon-mod.toml`, `assets/`, `data/` | the manifest's `api` | preview | [mod-files.md](mod-files.md), [mods.md](../mods.md#the-manifest), [mod-index.md](mod-index.md) |
| Native mod package | `.hnmod`, or a `.hmod` with native code: a zip with `horizon-native.json` and the bundle's `.rs` and `.toml` files | `schema` | experimental | [mod-files.md](mod-files.md), [native-mods.md](../native-mods.md) |
| Mods turned on | `horizon-mods.txt` | none | preview | [mods.md](../mods.md#playing-with-mods) |
| Horizon settings | `horizon-graphics.txt`, `horizon-extras.txt` (`key:value`, as `options.txt`) | none: unknown keys fall back to defaults | preview | [graphics.md](../graphics.md), [extras.md](../extras.md#options-storage) |
| Server settings | `server.toml` | its keys | stable | [server-reference.md](server-reference.md) |
+70
View File
@@ -0,0 +1,70 @@
# Mod files
The rules a `.hmod` or `.hnmod` follows, in one place, for tools that make or read them. The readers are
`horizon_mods::Package` and `horizon_mods::Content` (content), and `horizon_mods::index::native::Bundle` (native
code). Level: content is preview, native code experimental ([data-formats.md](data-formats.md)).
## The zip
- A zip archive. Entries are stored or deflate-compressed, the only methods the readers support.
- A path is relative and `/`-separated: no `\`, no `:`, no empty, `.` or `..` part, and it doesn't start with `/`.
Directory entries are skipped. A native entry must be a regular file (no symlinks or devices in the zip's Unix mode).
- Each path appears once.
## Top-level entries
What a file holds decides what it is (#5865):
| Entry | Means | Read by |
| --- | --- | --- |
| `horizon-mod.toml` | a content mod's manifest ([mods.md](../mods.md#the-manifest)) | `Package`, `Content` |
| `assets/…`, `data/…` | the content mod's resources and data | `Content` |
| `horizon-native.json` | native code: the manifest below | `Bundle` |
| `mixins/<id>/…` | the native bundle's sources ([native-mods.md](../native-mods.md#a-bundle)) | `Bundle` |
| `mod.wasm` | a legacy WebAssembly mod: it doesn't run any more | refused in a file with native code |
- **Content only:** `horizon-mod.toml` with `assets/` and `data/`. Nothing compiles.
- **Native code:** `horizon-native.json` and `mixins/<id>/`. The launcher builds it into the game.
- **Both, in one `.hmod`:** the two manifests name the same mod id. Content entries are checked as content and left
out of the build. Any other top-level entry refuses the file.
- **`.hnmod`:** native code only, read exactly as before.
The registry still publishes native mods as `.hnmod`. It switches to `.hmod` once launchers and the game read native
`.hmod`s, so no launcher in use gets a file it can't read.
## `horizon-native.json`
A JSON object. Unknown fields refuse the file.
| Field | Type | Rule |
| --- | --- | --- |
| `schema` | number | `1` |
| `id` | string | the mod id, `[a-z0-9_]{1,64}`, the same as `mixins/<id>/` and its `horizon-mixin.toml` `[mod] id` |
| `version` | string | not empty, the same as `horizon-mixin.toml` `[mod] version` |
| `game` | string | not empty, the Minecraft version (`"26.3"`) |
| `horizon` | string | the exact Horizon commit, 40 lowercase hex characters, the same as `[compatibility] horizon_revision` |
| `mixins` | string | the exact horizon-mixins commit, 40 lowercase hex characters |
| `hooks` | number, optional | the native hook points' version it needs, at least `1` ([native-mods.md](../native-mods.md#mods-on-the-hook-points)) |
## Under `mixins/<id>/`
`mixins/<id>/horizon-mixin.toml` must be there. Besides it, only:
- `.rs` and `.toml` files;
- files under `data/`, or under the `data/` of a top-level folder (`engine/data/`);
- `README*`, `LICENSE*`, `LICENCE*`, `COPYING*`, `NOTICE*` and `.gitignore`, in the bundle's folder or a top-level
folder.
## Content paths
`assets/` or `data/`, then at least two parts of `[a-z0-9_.-]` (vanilla's resource-location characters).
## Limits
| What | Limit |
| --- | --- |
| A registry file, as downloaded | 64 MiB |
| Native files (everything but content entries) | 512 entries, 64 MiB unpacked in total |
| Content files | 65,536 files, 16 MiB each, 512 MiB in total |
| `horizon-mod.toml` inside a native file | 1 MiB |
| `mod.wasm` (legacy) | 32 MiB |
+13 -3
View File
@@ -4,6 +4,14 @@ The 40 areas changed most often in the 30 days to 2026-10-23 (commits on main to
counted only for the game loop because nearly every gameplay change touches it). The crate map, how `Game` is
organised and how to build and test are in [AGENTS.md](../AGENTS.md).
Gameplay lives in the `horizon-client` library; `main.rs` is a thin executable calling
`entry::main`. The reusable modules, control tool and probes belong to `horizon-client-lib`,
whose library root is `crates/horizon-client/src/client_lib.rs`. Source paths stay in place.
The gameplay library re-exports the reusable API, keeping existing mod targets unchanged.
Gameplay tests run in its library test target, separately from the reusable library's tests.
There is no copy of gameplay in the executable's test target. Tools and probes depend only
on the reusable library, so gameplay edits do not invalidate their builds.
Paths: `L/` = `crates/horizon-client/src/`, `G/` = `crates/horizon-client/src/game/`, `W/` =
`crates/horizon-world/src/`, `K/` = `crates/horizon-client-core/src/`, `C/` = `crates/horizon-command/src/`,
`I/` = `crates/horizon-items/src/`, `R/` = `crates/horizon-client-render/src/`, `N/` = `crates/horizon-client-input/src/`.
@@ -12,8 +20,10 @@ Checks: a bare name is a harness (`python3 tools/parity.py <name>`, script `tool
For the full set of harnesses a file can affect, ask the runner:
`printf '%s\n' <paths> > /tmp/p.txt && python3 tools/parity.py --changed-files /tmp/p.txt --list`.
Any file of the game binary (`game.rs`, `game/`) selects the ~25 harnesses that run the binary; a client lib
file selects ~103.
Client probes are built with `-p horizon-client-lib`; the game with `-p horizon-client`.
Because the probe library does not compile gameplay, its dep-info does not include `game.rs`
or `game/`. Exact harness counts still depend on refreshed records and reachability; use the
command above to inspect the selection rather than relying on historical counts.
| # | Area (commits) | Files | Checks |
|---|---|---|---|
@@ -22,7 +32,7 @@ file selects ~103.
| 3 | World save and load (163) | `L/save.rs`, `G/saving.rs`, `G/persist.rs` (entities), `L/entity_nbt.rs`, `K/block_entity_nbt.rs`, `W/region.rs`, `W/nbt.rs` | save_roundtrip, world_reopen, world, datafix |
| 4 | World generation (153) | `W/worldgen.rs`, `W/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `placement/`), `L/vanillagen.rs` | chunk_generation, worldgen, server_world_gen, world_spawn, feature_graph, feature_placement, decoration and the other worldgen harnesses |
| 5 | Multiplayer client, packets in (145) | `G/remote.rs` (`fn handle_packet`, `fn tick_remote`), `G/remote_*.rs`, `L/net.rs` | dedicated_server, server_entities, lan_e2e (all) |
| 6 | Agent control, horizon-ctl (145) | `L/control.rs`, `L/bin/horizon-ctl/`, `G/control.rs`, `G/agent.rs`, `G/events.rs`, `G/ui_control.rs` | `python3 tools/agent_control_e2e.py`; `cargo test -p horizon-client --bin horizon-ctl` |
| 6 | Agent control, horizon-ctl (145) | `L/control.rs`, `L/bin/horizon-ctl/`, `G/control.rs`, `G/agent.rs`, `G/events.rs`, `G/ui_control.rs` | `python3 tools/agent_control_e2e.py`; `cargo test -p horizon-client-lib --bin horizon-ctl` |
| 7 | CI and release (134) | `.github/workflows/` (`ci.yml`, `parity.yml`, `release.yml`, `launcher.yml`), `tools/ci/`, `tools/parity.py`, `tools/pr_ready.py` | `python3 -m unittest discover -s tests`; actionlint (`.github/actionlint.yaml`) |
| 8 | LAN host: connections, play state, tracking (133) | `L/lan/` (`conn.rs`, `hub.rs` + `hub/`, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`), `G/lan.rs` | lan, lan_e2e (all), `cargo test -p horizon-client --test lan_bot` |
| 9 | Renderer core (132) | `R/gfx.rs` (`Renderer::new`, `Renderer::render`), `R/gfx/` (cull, occlusion, translucency, upload, timing, lod) | client_graphics, culling_rays, translucency; `horizon-client --benchmark standard` |
+1 -1
View File
@@ -46,7 +46,7 @@ The report is the [benchmark report format](benchmark-report.md): startup to the
## Reproduce it
You need Horizon built (`cargo build --release -p horizon-client --bins`) and its resources prepared from your own copy of 26.3 (which also provides `.local/minecraft/26.3`, the vanilla client the driver starts), Java 25 with `javac`, and Python 3.10 or newer.
You need Horizon built (`cargo build --release -p horizon-client -p horizon-client-lib --bins`) and its resources prepared from your own copy of 26.3 (which also provides `.local/minecraft/26.3`, the vanilla client the driver starts), Java 25 with `javac`, and Python 3.10 or newer.
```sh
python3 tools/perf_bench.py --game horizon --label horizon-mine
+1 -1
View File
@@ -68,7 +68,7 @@ Not implemented: anisotropic filtering (wgpu needs linear magnification for it,
`tools/graphics_bench.py` opens a fresh creative world (seed 1234) on a virtual screen and flies the camera along four fixed 192-block paths, over a village, a forest, the ocean and a mountain, at render distances 8, 16 and 32, one game per distance. Each flight takes 12 seconds and reports average FPS, 1% lows, frame-time percentiles, CPU time per frame (frame time minus waits for the GPU, the display and the limiter), the split of that CPU time (world update, ticks, frame preparation, renderer) for all frames and for the slowest 1%, and GPU time per frame from timestamp queries.
```sh
cargo build --release -p horizon-client --bins
cargo build --release -p horizon-client -p horizon-client-lib --bins
python3 tools/graphics_bench.py --label mine # lavapipe on vscreen: CPU-side numbers
python3 tools/graphics_bench.py --label mine-gpu --offscreen # the desktop's RTX 4080
python3 tools/graphics_bench.py --compare baseline-4080 mine-gpu # markdown table
+1 -1
View File
@@ -119,7 +119,7 @@ local socket; under control, losing window focus does not pause the game or
release held keys, so a script can play while you use other windows.
```sh
cargo build --release -p horizon-client --bins
cargo build --release -p horizon-client -p horizon-client-lib --bins
C=target/release/horizon-ctl
$C launch --world Survival --seed 5 --wait # start the game, open or create the world, wait for it
$C state # player, world, screen, crosshair target
+11 -1
View File
@@ -431,6 +431,16 @@ text files a crate folder carries (`README*`, `LICENSE*`, `COPYING*`, `NOTICE*`,
folder or a crate's), so a crate that compiles its README in builds the same from the registry as from a folder. At
most 512 files and 64 MiB unpacked; anything else refuses the bundle. A local folder build has no such limits.
**One `.hmod` for content and code.** A mod's content and its native code ship as one `.hmod`, and what is inside
decides what happens: only `horizon-mod.toml` with `assets/` and `data/` is a content mod, nothing compiles; with
`horizon-native.json` and `mixins/<id>/` (a bundle as above) it is a native mod, built into the game. Both may be in
one file: `horizon-mod.toml`, `assets/` and `data/` are its content, checked as a content mod's
(`horizon_mods::Content`) and left out of the build, and the two manifests name the same mod id. A `.hmod` with
native code is never a WebAssembly mod: one carrying `mod.wasm` is refused with
`horizon_mods::index::native::WASM_GONE`. An `.hnmod` (code only) keeps loading as before. The registry keeps
publishing native mods as `.hnmod` until launchers and the game read native `.hmod`s. Every rule of the file is in
[api/mod-files.md](api/mod-files.md).
### Post-build steps
A `[[post_build]]` names a bin of one of the mod's crates. After the game compiles, the launcher builds it with the
@@ -891,7 +901,7 @@ each was built or last used): switching a mod off and on again plays the kept bu
The file is pinned by SHA-256 like every registry file, and the version, game and pins are the bundle's own
`horizon-native.json`. `revoked.toml` takes a bad release down.
A content mod's entry may turn `native-mod` once (never back): its `.hmod` versions stay published and
are checked as `.hmod`s, the index lists only its `.hnmod` versions, and the launcher's update keeps the
are checked as `.hmod`s, the index lists only its versions with native code, and the launcher's update keeps the
player's `.hmod` (the game runs it until a native build has the mod, then skips it); removing the mod removes both.
- **Isolation of data.** Native mods run only in the instance they are installed in, with its worlds, options and
logs (the default Horizon instance's, when they are installed there); other instances play their own versions.
+1 -1
View File
@@ -3,7 +3,7 @@
Run with the pinned prepared client resources:
```sh
CARGO_BUILD_JOBS=6 cargo run --profile fast -p horizon-client --example portal_scan_bench -- \
CARGO_BUILD_JOBS=6 cargo run --profile fast -p horizon-client-lib --example portal_scan_bench -- \
.local/horizon-client/26.3
```
+1 -1
View File
@@ -5,7 +5,7 @@ Run with the pinned prepared client resources:
```sh
flock /tmp/horizon-optimizations-20261015-heavy.lock env CARGO_BUILD_JOBS=2 \
CARGO_TARGET_DIR=/home/bob/Documents/horizon/target \
cargo run --profile fast -p horizon-client --example random_tick_eligibility_bench -- \
cargo run --profile fast -p horizon-client-lib --example random_tick_eligibility_bench -- \
.local/horizon-client/26.3
```
+1 -1
View File
@@ -19,7 +19,7 @@ From this worktree, with no other benchmark running:
```sh
export CARGO_INCREMENTAL=0
export CARGO_TARGET_DIR=/home/bob/Documents/horizon/target
cargo build --profile fast -p horizon-client --example save_batch_bench
cargo build --profile fast -p horizon-client-lib --example save_batch_bench
taskset -c 2 "$CARGO_TARGET_DIR/fast/examples/save_batch_bench"
cargo test --profile fast -p horizon-client --lib save::tests
cargo clippy --profile fast -p horizon-client --all-targets -- -D warnings
+1 -1
View File
@@ -14,7 +14,7 @@ Use real prepared resources; no game files belong in Git. Build the baseline
at `89b58991bbc02eb79b6965fc4ca56357fe2297d9` with the benchmark example copied in
and `ItemRenderer::load(root)` in its item stage. Save that binary, then build
this branch with `ItemRenderer::load_with_library(root, assets.model_library.clone())`.
Both builds use `cargo build --locked --profile fast -p horizon-client --example
Both builds use `cargo build --locked --profile fast -p horizon-client-lib --example
model_library_bench` with `HORIZON_BENCH_COUNT_ALLOCATIONS` unset (`opt-level=3`, LTO disabled, 16 codegen units). Set
`CARGO_BUILD_JOBS=2`. On the optimization machine, hold
`/tmp/horizon-optimizations-20261015-heavy.lock` for each entire build or paired
+1 -1
View File
@@ -30,7 +30,7 @@ mod crates;
const PREFIX: &str = "native-";
/// horizon-mixins, for bundles that don't name the revision they were tested with.
const MIXINS_GIT: &str = "https://github.com/horizon-rust/horizon-mixins";
pub const MIXINS_REV: &str = "a2af5c0dd4117d51ecd70066ba7a00b4a2c10dab";
pub const MIXINS_REV: &str = "ddd9700b21997c95d95ac6ee5ecafe3830cf622a";
/// The features of the release's own game (game-build.yml).
const FEATURES: &str = "horizon-client/mods";
+22
View File
@@ -0,0 +1,22 @@
from pathlib import Path
import unittest
from unittest import mock
from tools import interaction_lab
class CargoExampleTests(unittest.TestCase):
def test_client_probe_uses_the_reusable_library_package(self):
with mock.patch.object(interaction_lab.subprocess, 'run') as run:
interaction_lab.cargo_example('horizon-client', 'menu_fixture', Path('cases.txt'), profile='fast')
run.assert_called_once_with(
['cargo', 'run', '-q', '-j3', '--profile', 'fast', '-p', 'horizon-client-lib',
'--example', 'client_probe', '--', 'menu_fixture', 'cases.txt'],
check=True, cwd=interaction_lab.lab.ROOT)
def test_other_probe_packages_are_unchanged(self):
with mock.patch.object(interaction_lab.subprocess, 'run') as run:
interaction_lab.cargo_example('horizon-world', 'loot_probe', 42)
run.assert_called_once_with(
['cargo', 'run', '-q', '-j3', '-p', 'horizon-world', '--example', 'world_probe', '--', 'loot_probe', '42'],
check=True, cwd=interaction_lab.lab.ROOT)
+45
View File
@@ -131,6 +131,51 @@ class ReachTests(unittest.TestCase):
self.assertIn("crates/b/src/used.rs", kept)
self.assertNotIn("crates/c/src/lib.rs", kept)
def test_external_library_and_probe_roots_follow_the_manifest(self):
workspace(self.root, {
"crates/b/Cargo.toml": '[package]\nname = "b"\nautobins = false\nautoexamples = false\n'
'[lib]\npath = "../a/src/shared.rs"\n'
'[[example]]\nname = "client_probe"\npath = "../a/examples/probe/main.rs"\n',
"crates/a/src/shared.rs": "pub mod used;\n",
"crates/a/src/lib.rs": "pub fn game_only() {}\n",
"crates/a/Cargo.toml": '[package]\nname = "a"\nautobins = false\nautoexamples = false\n',
"crates/a/examples/probe/main.rs": "extern crate b as legacy;\nfn main() { legacy::used::helper(); }\n",
"crates/b/src/main.rs": "compile_error!(\"disabled auto-bin\");\n",
"crates/b/examples/ignored.rs": "compile_error!(\"disabled auto-example\");\n",
})
graph = parity_reach.SourceGraph(self.root, parity.rust_code)
roots = parity_reach.crate_roots(self.root / "crates/b")
self.assertIn((self.root / "crates/a/src/shared.rs").resolve(), roots)
self.assertIn((self.root / "crates/a/examples/probe/main.rs").resolve(), roots)
self.assertNotIn((self.root / "crates/b/src/main.rs").resolve(), roots)
self.assertTrue(graph.is_library("crates/a/src/shared.rs"))
self.assertEqual(graph.entries([["example", "client_probe", "dev", ["-p", "a"], True]],
{"crates/a/examples/probe/main.rs"}), {"crates/a/examples/probe/main.rs"})
reached = graph.reachable({"crates/a/examples/probe/main.rs"},
{"crates/a/examples/probe/main.rs", "crates/a/src/shared.rs", "crates/a/src/used.rs"})
self.assertIn("crates/a/src/used.rs", reached)
self.assertEqual(graph.targets(self.root / "crates/b"),
{"client_probe": ("example", "crates/a/examples/probe/main.rs")})
def test_old_broad_client_records_find_moved_ctl_and_probes(self):
workspace(self.root, {
"crates/horizon-client/Cargo.toml": '[package]\nname = "horizon-client"\nautobins = false\nautoexamples = false\n'
'[[bin]]\nname = "horizon-client"\npath = "src/main.rs"\n',
"crates/horizon-client/src/main.rs": "fn main() {}\n",
"crates/horizon-client/src/bin/horizon-ctl.rs": "fn main() {}\n",
"crates/horizon-client/examples/probe/main.rs": "fn main() {}\n",
"crates/horizon-client-lib/Cargo.toml": '[package]\nname = "horizon-client-lib"\nautobins = false\nautoexamples = false\n'
'[[bin]]\nname = "horizon-ctl"\npath = "../horizon-client/src/bin/horizon-ctl.rs"\n'
'[[example]]\nname = "client_probe"\npath = "../horizon-client/examples/probe/main.rs"\n',
})
graph = parity_reach.SourceGraph(self.root, parity.rust_code)
game, ctl, probe = ("crates/horizon-client/" + path for path in
["src/main.rs", "src/bin/horizon-ctl.rs", "examples/probe/main.rs"])
self.assertEqual(graph.entries([["bins", "", "dev", ["-p", "horizon-client"], False]], {game, ctl, probe}),
{game, ctl})
self.assertEqual(graph.entries([["examples", "", "dev", ["-p", "horizon-client"], True]], {game, ctl, probe}),
{probe})
def test_unknown_probes_keep_everything(self):
self.assertEqual(set(self.narrowed(artifacts=[])), set(self.dependencies))
self.assertEqual(set(self.narrowed(artifacts=[["example", "missing", "release", ["-p", "a"], True]])),
+36
View File
@@ -30,6 +30,31 @@ class ParityRunnerTests(unittest.TestCase):
self.assertEqual(parity.artifacts_of(["cargo", "build", "--release", "-p", "horizon-world", "--bin=horizon-world"]),
[["bin", "horizon-world", "release", ["-p", "horizon-world"], False]])
def test_prebuild_follows_a_moved_target_without_losing_features(self):
selection = ["-p", "horizon-client", "--no-default-features", "--features", "horizon-client/rng-trace"]
artifacts = [["example", "client_probe", "fast", selection, True],
["bin", "horizon-ctl", "fast", selection, False]]
packages = {("example", "client_probe"): "horizon-client-lib", ("bin", "horizon-ctl"): "horizon-client-lib"}
commands = parity.prebuild_commands(artifacts, packages)
self.assertEqual(len(commands), 2)
for command in commands:
self.assertEqual(command[:5], ["cargo", "build", "--locked", "--profile", "fast"])
self.assertEqual(command[5:5 + len(selection)], selection)
self.assertEqual(command[5 + len(selection):7 + len(selection)], ["-p", "horizon-client-lib"])
self.assertEqual(artifacts[0][3], selection)
def test_old_broad_client_selections_include_the_moved_targets(self):
packages = {("bin", "horizon-client"): "horizon-client", ("bin", "horizon-ctl"): "horizon-client-lib",
("example", "client_probe"): "horizon-client-lib"}
for flag in ["--bins", "--examples"]:
command = ["cargo", "build", "-p", "horizon-client", "--no-default-features", flag]
artifacts = parity.artifacts_of(command)
before = [list(artifact[3]) for artifact in artifacts]
self.assertEqual(parity.prebuild_commands(artifacts, packages),
[["cargo", "build", "--locked", "--profile", "dev", "-p", "horizon-client",
"--no-default-features", "-p", "horizon-client-lib", flag]])
self.assertEqual([artifact[3] for artifact in artifacts], before)
def test_prebuild_repeats_each_harness_selection(self):
artifacts = [
*parity.artifacts_of(["cargo", "build", "--release", "-p", "horizon-world", "--example", "a"]),
@@ -604,5 +629,16 @@ class WorkspaceManifestTests(unittest.TestCase):
optional = {"crates/horizon-client/Cargo.toml": self.CLIENT + '\n[features]\nfast = ["wgpu/x"]\n'}
self.assertEqual(self.verdict({"crates/horizon-client/Cargo.toml": self.CLIENT}, optional), set())
def test_external_feature_sources_are_not_discarded(self):
with tempfile.TemporaryDirectory() as temp, mock.patch.object(parity, "ROOT", Path(temp)):
root = Path(temp)
(root / "crates/core").mkdir(parents=True)
(root / "shared").mkdir()
(root / "crates/core/Cargo.toml").write_text(
'[package]\nname = "core"\n[lib]\npath = "../../shared/lib.rs"\n'
'[features]\nextra = []\n')
(root / "shared/lib.rs").write_text('#[cfg(feature = "extra")]\nfn extra() {}\n')
self.assertFalse(parity.feature_sources_changed("core", "extra", {"crates/core/Cargo.toml"}))
def test_without_a_base_everything_counts(self):
self.assertEqual(parity.workspace_only_manifests({"Cargo.lock"}, None), set())
+11
View File
@@ -31,6 +31,17 @@ class PrReadyTests(unittest.TestCase):
# The workspace manifest and lock touch every crate.
self.assertIsNone(pr_ready.touched_crates({"Cargo.lock", "crates/horizon-core/src/lib.rs"}, PACKAGES))
def test_shared_client_sources_select_the_library_without_gameplay(self):
packages = {**PACKAGES, "horizon-client-lib": "crates/horizon-client-lib"}
for path in ["src/client_lib.rs", "src/health.rs", "src/lan/hub.rs", "src/bin/horizon-ctl/main.rs",
"examples/probe/main.rs", "tests/lan_bot.rs"]:
self.assertEqual(pr_ready.touched_crates({f"crates/horizon-client/{path}"}, packages),
["horizon-client-lib"])
for path in ["src/lib.rs", "src/main.rs", "src/entry.rs", "src/game.rs", "src/game/combat.rs",
"tests/launch_api.rs", "Cargo.toml", "build.rs"]:
self.assertEqual(pr_ready.touched_crates({f"crates/horizon-client/{path}"}, packages),
["horizon-client"])
def test_which_workspace_builds_a_path(self):
self.assertTrue(pr_ready.workspace_rust("crates/horizon-theme/fonts/x.ttf"))
self.assertTrue(pr_ready.workspace_rust("Cargo.toml"))
+1 -1
View File
@@ -963,7 +963,7 @@ def main():
if args.bin_dir:
bins = Path(args.bin_dir).resolve()
else:
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bins'],
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib', '--bins'],
cwd=ROOT, check=True)
meta = json.loads(subprocess.run(['cargo', 'metadata', '--format-version', '1', '--no-deps'], cwd=ROOT,
capture_output=True, text=True, check=True).stdout)
+1 -1
View File
@@ -66,7 +66,7 @@ def run_harness(name, oracle, example, inputs_text, extra_sources=(), java_args=
dependencies = os.pathsep.join(map(str, [jars['client'], *libraries]))
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-sourcepath', lab.ROOT / 'mods/horizon/src/test/java',
'-cp', dependencies, '-d', classes, oracle_path], 'compile-java')
run(['cargo', 'build', '--locked', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--locked', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--locked', '--no-deps', '--format-version', '1'], cwd=lab.ROOT))
binary = Path(metadata['target_directory']) / 'debug/examples' / ('client_probe.exe' if os.name == 'nt' else 'client_probe')
run([binary, example, *rust_args, inputs, build / 'rust.txt'], 'rust')
+1 -1
View File
@@ -261,7 +261,7 @@ def main():
if args.bin_dir:
bins = Path(args.bin_dir).resolve()
else:
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bins'],
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib', '--bins'],
cwd=ROOT, check=True)
meta = json.loads(subprocess.run(['cargo', 'metadata', '--format-version', '1', '--no-deps'], cwd=ROOT,
capture_output=True, text=True, check=True).stdout)
+1 -1
View File
@@ -170,7 +170,7 @@ def main():
return
ctl = Path(args.build).resolve() / "horizon-ctl"
if not ctl.exists():
sys.exit(f"{ctl} missing: cargo build --release -p horizon-client --bins")
sys.exit(f"{ctl} missing: cargo build --release -p horizon-client -p horizon-client-lib --bins")
cwd = ROOT / ".local/graphics-bench"
shutil.rmtree(cwd / "saves" / WORLD, ignore_errors=True)
cwd.mkdir(parents=True, exist_ok=True)
+2
View File
@@ -51,6 +51,8 @@ def cargo_example(package, example, *args, profile=None):
if profile:
command += ['--profile', profile]
probe = package.removeprefix('horizon-') + '_probe' # each crate's probe example: client_probe, world_probe
if package == 'horizon-client':
package = 'horizon-client-lib'
command += ['-p', package, '--example', probe, '--', example, *map(str, args)]
subprocess.run(command, check=True, cwd=lab.ROOT)
+1 -1
View File
@@ -6576,7 +6576,7 @@ def main():
if args.bin_dir:
bins = Path(args.bin_dir).resolve()
else:
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client',
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib',
'--bin', 'horizon-client', '--bin', 'horizon-ctl'],
cwd=ROOT, check=True)
meta = json.loads(subprocess.run(['cargo', 'metadata', '--format-version', '1', '--no-deps'], cwd=ROOT,
+1 -1
View File
@@ -237,7 +237,7 @@ def main():
subprocess.run(command, check=True)
if not facts.exists():
sys.exit(f'{facts}: no oracle facts (run with --jars, or tools/neoforge_oracle.py export)')
subprocess.run(['cargo', 'build', '--release', '-q', '-p', 'horizon-client', '--example', 'mod_port_probe'],
subprocess.run(['cargo', 'build', '--release', '-q', '-p', 'horizon-client-lib', '--example', 'mod_port_probe'],
cwd=ROOT, check=True)
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'],
cwd=ROOT, text=True))
+1 -1
View File
@@ -42,7 +42,7 @@ def close(g):
def build():
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client',
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client-lib', '--bin', 'horizon-client',
'--bin', 'horizon-ctl', '--example', 'old_world_fixture'], cwd=lan.ROOT, check=True)
meta = json.loads(subprocess.run(['cargo', 'metadata', '--format-version', '1', '--no-deps'], cwd=lan.ROOT,
capture_output=True, text=True, check=True).stdout)
+1 -1
View File
@@ -35,7 +35,7 @@ def compile_oracle(build, oracle_name, jars, libraries, java_major):
def build_probe(build, example):
"""Build the horizon-client probe binary; returns the command that runs probe `example`."""
run_logged(['cargo', 'build', '--locked', '-p', 'horizon-client', '--example', 'client_probe'],
run_logged(['cargo', 'build', '--locked', '-p', 'horizon-client-lib', '--example', 'client_probe'],
build, 'compile-rust-' + example, env=dict(os.environ, CARGO_BUILD_JOBS='3'))
import json
metadata = json.loads(subprocess.check_output(
+1 -1
View File
@@ -3,7 +3,7 @@
tarball for one of your own Linux or macOS machines. The resources are Mojang
assets: keep the archive in ignored .local/ and never publish or commit it.
cargo build --release -p horizon-client --bins
cargo build --release -p horizon-client -p horizon-client-lib --bins
python3 tools/package_native_client.py
"""
import argparse
+12
View File
@@ -644,6 +644,15 @@ def prebuild_commands(artifacts, packages=None):
if len(artifact) < 5:
continue
kind, name, profile, selection, examples = artifact
owner = (packages or {}).get((kind, name))
selected = [value for flag, value in zip(selection, selection[1:]) if flag == "-p"]
if not name and kind in ("bins", "examples") and "horizon-client" in selected \
and "horizon-client-lib" in (packages or {}).values():
# Broad records predate the client package split too.
owner = "horizon-client-lib"
if owner and selected and owner not in selected:
# A recorded target may have moved packages; retain its feature flags and old selections.
selection = [*selection, "-p", owner]
targets = groups.setdefault((profile, tuple(selection), examples), [])
target = [f"--{kind}", name] if name else [f"--{kind}"]
if target not in targets:
@@ -1366,6 +1375,9 @@ def feature_sources_changed(crate, feature, changed):
break
else:
return False
if any(not root.is_relative_to(manifest.parent.resolve()) for root in parity_reach.crate_roots(manifest.parent)):
# A directory-only scan cannot prove that shared external sources changed too.
return False
table = re.search(r"^\[features\]\n(.*?)(?=^\[|\Z)", text, re.M | re.S)
implies = {name: re.findall(r'"([^"]+)"', value) for name, value in
re.findall(r'^([\w-]+)\s*=\s*\[([^\]]*)\]', table.group(1) if table else "", re.M)}
+48 -10
View File
@@ -192,13 +192,33 @@ def parse_items(code, start, end, items):
"mentions": sorted(words(text)), "paths": sorted(path_words(text)), "qualified": sorted(qualified_words(text)), "invokes": sorted(set(MACRO_USE.findall(text)))})
def library_root(crate_dir):
"""Cargo's library source, including an explicit path outside the package directory."""
manifest = crate_dir / "Cargo.toml"
text = manifest.read_text(encoding="utf-8") if manifest.is_file() else ""
table = re.search(r"^\[lib\]([^\[]*)", text, re.M)
path = re.search(r'^path\s*=\s*"([^"\n]+)"', table.group(1), re.M) if table else None
return (crate_dir / (path.group(1) if path else "src/lib.rs")).resolve()
def crate_roots(crate_dir):
"""A crate's root files: its library, binaries and examples."""
roots = [crate_dir / "src" / "lib.rs", crate_dir / "src" / "main.rs"]
for folder in (crate_dir / "src" / "bin", crate_dir / "examples"):
if folder.is_dir():
roots += sorted(folder.glob("*.rs")) + sorted(folder.glob("*/main.rs"))
return [root for root in roots if root.is_file()]
path = crate_dir / "Cargo.toml"
manifest = path.read_text(encoding="utf-8") if path.is_file() else ""
roots = [library_root(crate_dir)]
if not re.search(r"^autobins\s*=\s*false", manifest, re.M):
roots.append(crate_dir / "src" / "main.rs")
folder = crate_dir / "src" / "bin"
roots += sorted(folder.glob("*.rs")) + sorted(folder.glob("*/main.rs"))
if not re.search(r"^autoexamples\s*=\s*false", manifest, re.M):
folder = crate_dir / "examples"
roots += sorted(folder.glob("*.rs")) + sorted(folder.glob("*/main.rs"))
for _, table in re.findall(r"^\[\[(bin|example)\]\]([^\[]*)", manifest, re.M):
path = re.search(r'^path\s*=\s*"([^"\n]+)"', table, re.M)
if path:
roots.append(crate_dir / path.group(1))
return [root.resolve() for root in roots if root.is_file()]
class SourceGraph:
@@ -215,6 +235,8 @@ class SourceGraph:
self.parent = {} # module file -> the file declaring it
self.module_name = {}
self.roots = set()
self.library_roots = {manifest.parent.name: self.relative(library_root(manifest.parent))
for manifest in (self.root / "crates").glob("*/Cargo.toml")}
self.target_cache = {}
self.workspace_heads = None
self.test_only = set() # modules compiled into tests only
@@ -238,6 +260,16 @@ class SourceGraph:
packages = [value for flag, value in zip(selection, selection[1:]) if flag == "-p"] or \
[manifest.parent.name for manifest in sorted((self.root / "crates").glob("*/Cargo.toml"))]
roots = set()
if name and not any(name in self.targets(self.root / "crates" / package) for package in packages):
owners = [manifest.parent.name for manifest in (self.root / "crates").glob("*/Cargo.toml")
if self.targets(manifest.parent).get(name, (None,))[0] == kind.rstrip("s")]
if len(owners) == 1:
packages = [*packages, owners[0]]
if not name and kind in ("bins", "examples") and "horizon-client" in packages \
and "horizon-client-lib" not in packages \
and (self.root / "crates/horizon-client-lib/Cargo.toml").is_file():
# Broad records predate the client package split too.
packages = [*packages, "horizon-client-lib"]
for package in packages:
crate = self.root / "crates" / package
targets = self.targets(crate)
@@ -270,9 +302,11 @@ class SourceGraph:
found = {}
manifest = (crate / "Cargo.toml").read_text(encoding="utf-8") if (crate / "Cargo.toml").is_file() else ""
package = re.search(r'^\[package\][^\[]*?^name\s*=\s*"([^"]+)"', manifest, re.MULTILINE | re.DOTALL)
if (crate / "src" / "main.rs").is_file() and package:
if (crate / "src" / "main.rs").is_file() and package and not re.search(r"^autobins\s*=\s*false", manifest, re.M):
found[package.group(1)] = ("bin", self.relative(crate / "src" / "main.rs"))
for kind, folder in (("bin", crate / "src" / "bin"), ("example", crate / "examples")):
if re.search(rf"^auto{kind}s\s*=\s*false", manifest, re.M):
continue
for path in sorted(folder.glob("*.rs")) + sorted(folder.glob("*/main.rs")):
found[path.stem if path.name != "main.rs" else path.parent.name] = (kind, self.relative(path))
for kind, table in re.findall(r"^\[\[(bin|example)\]\]([^\[]*)", manifest, re.MULTILINE):
@@ -289,7 +323,7 @@ class SourceGraph:
return self.root_of.get(path, path)
def is_library(self, path):
return self.target(path).endswith("/src/lib.rs")
return self.target(path) in self.library_roots.values()
def relative(self, path):
return Path(path).resolve().relative_to(self.root.resolve()).as_posix()
@@ -357,6 +391,10 @@ class SourceGraph:
declarations.append([match.group(1), [name for name, _ in stack], attribute.group(1) if attribute else None,
bool(TEST_MODULE.search(window))])
items = []
# An extern-crate alias still links its source crate, even when callers use the alias.
for match in re.finditer(r"\bextern\s+crate\s+(\w+)(?:\s+as\s+\w+)?\s*;", code):
items.append({"kind": "glue", "names": [], "mentions": [match.group(1)],
"paths": [match.group(1)], "qualified": [], "invokes": []})
parse_items(code, 0, len(code), items)
self.files[path] = parsed = {
"declarations": declarations,
@@ -608,7 +646,7 @@ class SourceGraph:
try:
for path in universe:
if path in entries or path not in self.parent and not path.endswith("/src/lib.rs"):
if path in entries or path not in self.parent and not self.is_library(path):
open_module(path, "entry, or a file no module places")
for node in seeds:
reach(node, "entry" if node[0] in entries else "always counted")
@@ -627,7 +665,7 @@ class SourceGraph:
# A library's root opens once reached code names the crate (`horizon_world::`, `use horizon_command
# as command`): its items are out of reach until then.
for word in (source["mentions"] | source["paths"]) & idents.keys():
root = f"crates/{idents[word]}/src/lib.rs"
root = self.library_roots[idents[word]]
if root in universe and root not in opened:
open_module(root, f"{why} names {word}")
# Plain mentions reach items of this file and of the modules above it.
@@ -643,7 +681,7 @@ class SourceGraph:
for name in item["names"]:
for target in sees.get(crate, {crate}):
for module in list(by_module.get((target, name), ())) + \
([f"crates/{idents[name]}/src/lib.rs"] if name in idents else []):
([self.library_roots[idents[name]]] if name in idents else []):
if module not in globbed and module in universe:
globbed.add(module)
wake([(module, index, None) for index in range(len(self.parse(module)["items"]))]
+1 -1
View File
@@ -244,7 +244,7 @@ def gen_vanilla_order(root, version, store, build):
"dev.horizonlab.parity.VanillaCommandParseOracle", "tree", build], build)
env = dict(os.environ, CARGO_TARGET_DIR=str(root / ".local" / "port-target"))
jobs = os.environ.get("FLEET_LOAD_JOBS")
run(["cargo", "build", "-p", "horizon-client", "--example", "client_probe",
run(["cargo", "build", "-p", 'horizon-client-lib', "--example", "client_probe",
*(["-j", jobs] if jobs else [])], root, env=env)
fixture = root / ".local/port-target/debug/examples/client_probe"
run([fixture, "command_parse_fixture", "tree", build / "rust_tree.json"], root)
+8 -1
View File
@@ -97,6 +97,13 @@ def touched_crates(paths, packages):
for path in paths:
if is_doc(path):
continue
if "horizon-client-lib" in packages and path.startswith("crates/horizon-client/"):
relative = path.removeprefix("crates/horizon-client/")
gameplay = relative in {"Cargo.toml", "build.rs", "windows-segment-heap.manifest",
"src/lib.rs", "src/main.rs", "src/entry.rs", "src/game.rs", "tests/launch_api.rs"} \
or relative.startswith("src/game/")
crates.add("horizon-client" if gameplay else "horizon-client-lib")
continue
owners = [name for name, directory in packages.items() if path.startswith(directory.rstrip("/") + "/")]
if owners:
crates.add(max(owners, key=lambda name: len(packages[name])))
@@ -328,7 +335,7 @@ def mod_features(crates):
# they are #[ignore]d and run here only when one of these changes.
CONTENT_ROOT = ("crates/horizon-client/src/mod_content", "crates/horizon-client-render/src/assets.rs", "crates/horizon-items/",
"crates/horizon-mods/", "crates/horizon-assets/")
CONTENT_ROOT_TESTS = "cargo test --locked -p horizon-client --lib mod_content::tests:: -- --ignored"
CONTENT_ROOT_TESTS = "cargo test --locked -p horizon-client-lib --lib mod_content::tests:: -- --ignored"
def plan(args, base, changed, crates_of, libraries, dependencies):
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Profile the validation and typed-deserialization passes in separate processes.
Build `cargo build --release -p horizon-client --example gameplay_decode_profile`
Build `cargo build --release -p horizon-client-lib --example gameplay_decode_profile`
then run this tool with the matching prepared resources. These passes are
both currently required for exact JSON error classification/precedence.
"""
+1 -1
View File
@@ -18,7 +18,7 @@ console` attaches through a pipe, runs two commands and shows what they log.
Everything runs in a fresh temporary folder with fresh HORIZON_HOME and XDG folders and no audio. Logs go
to --out. Build first:
cargo build --profile fast -p horizon-client --bins -p horizon-bots -p horizon-server
cargo build --profile fast -p horizon-client -p horizon-client-lib --bins -p horizon-bots -p horizon-server
"""
import argparse
import http.client
+1 -1
View File
@@ -214,7 +214,7 @@ def main():
if args.bin_dir:
bins = Path(args.bin_dir).resolve()
else:
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bins'],
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib', '--bins'],
cwd=ROOT, check=True)
bins = ROOT / 'target/fast'
out = Path(args.out)
+1 -1
View File
@@ -16,7 +16,7 @@ ops.json); at level 2 the admins' commands must do nothing.
The game and the server run with fresh HORIZON_HOME, launcher and XDG folders and without audio. Logs go to --out.
Build first:
cargo build --profile fast -p horizon-client --bins
cargo build --profile fast -p horizon-client -p horizon-client-lib --bins
cargo build --profile fast -p horizon-bots
"""
import argparse
+1 -1
View File
@@ -241,7 +241,7 @@ def main():
expected = 3 * operations + len(fixtures)
(build / 'inputs.txt').write_text(''.join(json.dumps(f, separators=(',', ':')) + '\n' for f in fixtures))
input_hash = lab.digest(build / 'inputs.txt', 'sha256')
run(['cargo', 'build', '--locked', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--locked', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--locked', '--no-deps', '--format-version', '1'], cwd=lab.ROOT))
binary = Path(metadata['target_directory']) / 'debug/examples' / ('client_probe.exe' if os.name=='nt' else 'client_probe')
run([binary, 'arrow_probe',resources, build, '--describe'], 'describe')
+1 -1
View File
@@ -279,7 +279,7 @@ def main():
*(PARITY / f'{n}.java' for n in ('ParityData', 'VanillaBlockEntityAnimOracle'))], 'compile-java')
run(['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaBlockEntityAnimOracle',
inputs, build / 'vanilla.txt'], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe', '--',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe', '--',
'block_entity_anim_fixture', resources, inputs, build / 'rust.txt'], 'rust')
def split(path):
+1 -1
View File
@@ -303,7 +303,7 @@ def main():
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text().splitlines()]
textures = sorted({s['texture'] for v in vanilla for s in v.get('submits', []) if 'texture' in s})
(build / 'textures.txt').write_text(''.join(t + '\n' for t in textures))
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe', '--',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe', '--',
'block_entity_render_fixture', resources, inputs, build / 'textures.txt', build / 'rust.jsonl'], 'rust')
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text().splitlines()]
assert len(vanilla) == len(rust), (len(vanilla), len(rust))
+1 -1
View File
@@ -119,7 +119,7 @@ def main():
'compile-java')
run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
'dev.horizonlab.parity.VanillaBlockEntityTickerOracle', inputs, build / 'vanilla.txt'], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example',
'client_probe', '--', 'block_entity_tickers_fixture', inputs, build / 'rust.txt'], 'rust')
matched = base.compare(build / 'vanilla.txt', build / 'rust.txt')
ticks = sum(1 for line in lines if line == 'tick')
+1 -1
View File
@@ -64,7 +64,7 @@ def main():
registries = json.loads((build / 'registries.json').read_text())
corpus = corpus_module.corpus(tree, registries)
(build / 'corpus.txt').write_text(''.join(line + '\n' for line in corpus), encoding='utf-8')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'fast/examples' / ('client_probe.exe' if os.name=='nt' else 'client_probe')
resources = lab.RESOURCES
+1 -1
View File
@@ -57,7 +57,7 @@ def main():
with (build / (name + '.log')).open('w', encoding='utf-8') as log:
base.run(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT)
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none','-sourcepath',str(lab.ROOT/'mods/horizon/src/test/java'), '-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'fast/examples' / ('client_probe.exe' if os.name=='nt' else 'client_probe')
run([binary, 'enchanting_fixture',resources, inputs, build / 'rust.txt'], 'rust')
+1 -1
View File
@@ -149,7 +149,7 @@ def main():
'-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx2G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]),
'dev.horizonlab.parity.VanillaEntityDeathLootOracle', inputs, build / 'server.txt'], 'server')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'fast/examples/client_probe'
run([binary, 'entity_death_loot_fixture', resources, inputs, build / 'rust.txt'], 'rust')
+1 -1
View File
@@ -381,7 +381,7 @@ def main():
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp', dependencies, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaEntityAnimOracle',
inputs, build / 'vanilla.jsonl', jars['client']], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe', '--',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe', '--',
'entity_anim_fixture', resources, build / 'vanilla.jsonl', build / 'rust.jsonl'], 'rust')
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text().splitlines()]
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text().splitlines()]
+1 -1
View File
@@ -66,7 +66,7 @@ def main():
'-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx1G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]),
'dev.horizonlab.parity.VanillaEnvironmentOracle', inputs, build / 'vanilla.jsonl'], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe', '--',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe', '--',
'environment_fixture', resources, inputs, build / 'rust.jsonl'], 'rust')
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text().splitlines()]
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text().splitlines()]
+1 -1
View File
@@ -148,7 +148,7 @@ def main():
print(f'{len(vanilla)} oracle lines')
return
halloween = vanilla[0].split()[1]
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'fast/examples/client_probe'
run([binary, 'finalize_spawn_fixture', resources, inputs, build / 'rust.txt', halloween], 'rust')
+1 -1
View File
@@ -15,7 +15,7 @@ from integration_env import suite_lock
if __name__ == '__main__':
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client',
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib',
'-p', 'horizon-relay', '--bin', 'horizon-client', '--bin', 'horizon-ctl',
'--bin', 'horizon-relay'], cwd=lab.ROOT, check=True)
metadata = json.loads(subprocess.check_output(
+1 -1
View File
@@ -34,7 +34,7 @@ def main():
subprocess.run(['java', '-Xmx512M', '-cp', str(classes) + os.pathsep + classpath,
'dev.horizonlab.parity.VanillaHeldDropOracle', str(build / 'vanilla.txt')], check=True)
with (build / 'rust.txt').open('w') as out:
subprocess.run(['cargo', 'run', '--quiet', '-p', 'horizon-client', '--example',
subprocess.run(['cargo', 'run', '--quiet', '-p', 'horizon-client-lib', '--example',
'client_probe', '--', 'held_drop_fixture'], cwd=lab.ROOT, stdout=out, check=True)
matched = base.compare(build / 'vanilla.txt', build / 'rust.txt')
print(f'Held drop parity passed: {matched} inventory/drop/swing/packet snapshots match vanilla')
+1 -1
View File
@@ -114,7 +114,7 @@ def main():
*(PARITY / f'{n}.java' for n in ('ParityData', 'ParityFont', 'VanillaHudOracle'))], 'compile-java')
run(['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaHudOracle',
inputs, build / 'vanilla.jsonl'], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe', '--',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe', '--',
'hud_fixture', resources, inputs, build / 'rust.jsonl'], 'rust')
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text().splitlines()]
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text().splitlines()]
+1 -1
View File
@@ -91,7 +91,7 @@ def main():
if not (dimension / 'region').is_dir():
dimension = run / 'world'
for extra in (['--chunks', '0', '0', '2', '3'], ['--incremental', '100']):
status = subprocess.run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client',
status = subprocess.run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib',
'--example', 'client_probe', '--', 'light_parity', str(RESOURCES), str(dimension), *extra],
cwd=ROOT).returncode
if status:
+1 -1
View File
@@ -44,7 +44,7 @@ def main():
classes=build/'classes';classes.mkdir(exist_ok=True)
dependencies=os.pathsep.join(map(str,[jars['client'],*libraries]))
run(['javac','--release',lock['minecraft']['java_major'],'-proc:none','-sourcepath',str(lab.ROOT/'mods/horizon/src/test/java'),'-cp',dependencies,'-d',classes,oracle],'compile-java')
run(['cargo','build','--locked','-p','horizon-client','--example','client_probe'],'compile-rust')
run(['cargo','build','--locked','-p','horizon-client-lib','--example','client_probe'],'compile-rust')
metadata=json.loads(subprocess.check_output(['cargo','metadata','--locked','--no-deps','--format-version','1'],cwd=lab.ROOT))
binary=Path(metadata['target_directory'])/'debug/examples'/('client_probe.exe' if os.name=='nt' else 'client_probe')
run([binary,'input_probe',inputs,build/'rust.txt'],'rust')
+1 -1
View File
@@ -663,7 +663,7 @@ def main():
'dev.horizonlab.parity.VanillaMobAiOracle', inputs, build / 'vanilla.txt', world],
build, 'vanilla', timeout=3600)
if not args.java_only:
command = ['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe']
command = ['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe']
target, env = lab.cargo_target(), None
if args.trace:
target = lab.LOCAL / 'mob-ai-trace-target'
+1 -1
View File
@@ -62,7 +62,7 @@ def main():
subprocess.run(list(map(str, cmd)), cwd=lab.ROOT, check=True, timeout=3600, stdout=log, stderr=subprocess.STDOUT)
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx1G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]), 'dev.horizonlab.parity.VanillaMobAttributesOracle', build / 'vanilla.txt'], 'java')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
binary = lab.cargo_target() / 'fast/examples/client_probe'
resources = lab.RESOURCES
run([binary, 'mob_attributes_fixture', resources, build / 'rust.txt'], 'rust')
+1 -1
View File
@@ -82,7 +82,7 @@ def main():
dependencies = os.pathsep.join(map(str, libraries))
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp',
str(jars['server'])+os.pathsep+dependencies, '-d', classes, oracle, *helpers], 'compile-java')
run(['cargo', 'build', '--locked', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--locked', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--locked', '--no-deps', '--format-version', '1'], cwd=lab.ROOT))
executable = Path(metadata['target_directory'])/'debug/examples'/('client_probe.exe' if os.name=='nt' else 'client_probe')
cases = []
+1 -1
View File
@@ -807,7 +807,7 @@ def main():
damage_dir=build/'damage';damage_dir.mkdir(exist_ok=True)
for target, source in [('types.json','damage-types.json'),('tags.json','damage-tags.json')]:
(damage_dir/target).write_bytes((resources/source).read_bytes())
run(['cargo','build','--locked','-p','horizon-client','--example','client_probe'],'compile-rust')
run(['cargo','build','--locked','-p','horizon-client-lib','--example','client_probe'],'compile-rust')
metadata=json.loads(subprocess.check_output(['cargo','metadata','--locked','--no-deps','--format-version','1'],cwd=lab.ROOT))
binary=Path(metadata['target_directory'])/'debug/examples'/('client_probe.exe' if os.name=='nt' else 'client_probe')
run([binary,'scene_motion_probe',resources,build,'--describe'],'describe')
+1 -1
View File
@@ -569,7 +569,7 @@ def main():
'-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx3G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]),
'dev.horizonlab.parity.VanillaNaturalSpawnOracle', inputs, build / 'server.txt'], 'server')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'fast/examples/client_probe'
run([binary, 'natural_spawn_fixture', resources, inputs, build / 'rust.txt'], 'rust')
+1 -1
View File
@@ -71,7 +71,7 @@ def main():
*(PARITY / f'{n}.java' for n in ('ParityData', 'VanillaPackSelectionOracle'))], 'compile-java')
run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaPackSelectionOracle',
inputs, build / 'vanilla.jsonl'], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe', '--',
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe', '--',
'pack_selection_fixture', inputs, build / 'rust.jsonl'], 'rust')
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text().splitlines()]
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text().splitlines()]
+1 -1
View File
@@ -335,7 +335,7 @@ def main():
'-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx3G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]),
'dev.horizonlab.parity.VanillaRaidOracle', inputs, build / 'server.txt'], 'server')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client-lib', '--example', 'client_probe'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'fast/examples/client_probe'
run([binary, 'raid_fixture', resources, inputs, build / 'rust.txt'], 'rust')
+1 -1
View File
@@ -80,7 +80,7 @@ def check_thrown(host, guest, out):
def main():
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client',
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib',
'--bin', 'horizon-client', '--bin', 'horizon-ctl'], cwd=lab.ROOT, check=True)
metadata = json.loads(subprocess.check_output(
['cargo', 'metadata', '--format-version', '1', '--no-deps'], cwd=lab.ROOT, text=True))
+1 -1
View File
@@ -11,7 +11,7 @@ import lan_e2e as lan
def main():
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client',
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib',
'--bin', 'horizon-client', '--bin', 'horizon-ctl'], cwd=lab.ROOT, check=True)
metadata = json.loads(subprocess.check_output(
['cargo', 'metadata', '--format-version', '1', '--no-deps'], cwd=lab.ROOT, text=True))
+1 -1
View File
@@ -44,7 +44,7 @@ def main():
args = parser.parse_args()
bin_dir = Path(args.bin_dir).resolve() if args.bin_dir else None
if bin_dir is None:
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bins'], cwd=ROOT, check=True)
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '-p', 'horizon-client-lib', '--bins'], cwd=ROOT, check=True)
bin_dir = ROOT / os.environ.get('CARGO_TARGET_DIR', 'target') / 'fast'
shutil.rmtree(RUN, ignore_errors=True)
RUN.mkdir(parents=True)
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more