Compare commits

..
1384 changed files with 40434 additions and 73683 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
# Generated files that tests compare byte for byte with what they generate: LF on every OS, also
# where Git for Windows' core.autocrlf would check them out with CRLF.
docs/launcher-commands.md text eol=lf
docs/horizon-ctl-commands.md text eol=lf
website/src/tokens.css text eol=lf
docs/api/*-reference.md text eol=lf
+1
View File
@@ -6,6 +6,7 @@
/.github/ @ludovic111
/tools/ci/ @ludovic111
/tools/release_train.py @ludovic111
/tools/release_channels.py @ludovic111
/crates/horizon-release-sign/ @ludovic111
/crates/horizon-server/src/update.rs @ludovic111
/crates/horizon-server/Cargo.toml @ludovic111
-1
View File
@@ -1 +0,0 @@
0.2
+97 -12
View File
@@ -18,10 +18,6 @@ concurrency:
# A new push to a pull request replaces its running checks; main's runs all finish (releases follow them).
group: checks-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
verify:
runs-on: ubuntu-latest
@@ -61,17 +57,12 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable fmt --all -- --check
# With the game's code mods (on in every release build, off by default).
cargo +stable clippy --workspace --all-targets --features horizon-client/mods -- -D warnings
cargo +stable clippy --workspace --all-targets -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
# type-checks and the parity job builds and runs (they were most of this step's build and link time).
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable test --workspace --lib --bins --tests --features horizon-client/mods,horizon-mods/pack
cargo +stable test --workspace --doc --features horizon-client/mods,horizon-mods/pack
run: cargo +stable test --workspace
parity:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/parity.yml
@@ -430,7 +421,7 @@ jobs:
env:
RUSTUP_TOOLCHAIN: stable
run: python tools/test_float_log_parity.py
jfr-analysis:
portable-mod:
if: github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, linux, horizon-game]
steps:
@@ -442,7 +433,101 @@ jobs:
with:
distribution: temurin
java-version: '25'
- name: Compile original mod and test metrics
run: python tools/build_mod.py
- name: Verify streaming JFR analysis against the JDK exporter
run: python tools/test_jfr.py
- name: Verify ZGC pause and allocation event interpretation
run: python tools/test_jfr.py --gc zgc
- name: Build pinned portable packs
run: python tools/build_pack.py
- name: Measure isolated worker pacing on this platform
run: python -c "from tools.build_mod import benchmark_workers; benchmark_workers()"
- name: Measure isolated debounce queue polling
run: python -c "from tools.build_mod import benchmark_debounce; benchmark_debounce()"
- name: Measure wakeable idle queue latency
run: python -c "from tools.build_mod import benchmark_idle; benchmark_idle()"
- name: Measure object allocations with compact headers off and on
run: python -c "from tools.build_mod import benchmark_headers; benchmark_headers()"
- name: Test pack integrity and comparisons
run: python -m unittest discover -s tests -v
- name: Start recommended OpenGL macOS clients and verify framebuffer readback
if: runner.os == 'macOS'
run: |
python tools/test_client_graphics.py --native-display
cp dist/client-graphics-smoke.json dist/client-graphics-macos-title.json
python tools/test_client_graphics.py --native-display --world
cp dist/client-graphics-smoke.json dist/client-graphics-macos-world.json
- name: Compile the private dispatch fixture
run: python -c "from tools.build_mod import build_dispatch_fixture; build_dispatch_fixture()"
- name: Compile the private integrated-server graphics fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('server-graphics', 'dev.horizonlab.metrics.ServerGraphicsIntegration', fixture_dependencies={})"
- name: Compile the private tick-manager state fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('tick-state', 'dev.horizonlab.metrics.TickStateIntegration')"
- name: Compile the private terrain tick membership fixture
run: python -c "from tools.build_mod import build_tick_dedup_fixture; build_tick_dedup_fixture()"
- name: Compile the private culling whitelist fixture
run: python -c "from tools.build_mod import build_culling_empty_fixture; build_culling_empty_fixture()"
- name: Compile the private culling ray geometry fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('culling-rays', 'dev.horizonlab.dispatch.CullingRayIntegration', ['entityculling'], ['CullingRayAccess'])"
- name: Compile the private terrain position lookup fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-position', 'dev.horizonlab.dispatch.PositionIntegration')"
- name: Compile the private terrain quad reuse fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('quad-reuse', 'dev.horizonlab.dispatch.QuadReuseIntegration')"
- name: Compile the private object-size agent and terrain quad fixture
run: python -c "from tools.test_quad_sizes import build_agent; from tools.build_mod import build_integration_fixture; build_agent(); build_integration_fixture('quad-size', 'dev.horizonlab.dispatch.QuadSizeIntegration')"
- name: Compile the private terrain read shutdown fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-read', 'dev.horizonlab.dispatch.TerrainReadIntegration')"
- name: Install headless graphics libraries for applied-hook checks
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb libgl1 libegl1 libvulkan1 mesa-vulkan-drivers libasound2t64 libxkbcommon0
- name: Verify integrated-server graphics report against the actual device
run: python tools/test_server_graphics.py ${{ runner.os == 'macOS' && '--native-display' || '' }}
- name: Exercise the applied dispatcher hooks and upstream rejection path
if: runner.os == 'Linux'
run: python tools/test_dispatch.py
- name: Verify applied tick membership transitions and allocation
if: runner.os == 'Linux'
run: python tools/test_tick_dedup.py
- name: Verify live culling whitelist registration and empty iteration
if: runner.os == 'Linux'
run: python tools/test_culling_empty.py
- name: Compare applied culling ray geometry and provider queries
if: runner.os == 'Linux'
run: python tools/test_culling_rays.py
- name: Verify actual full-data position lookup equivalence
if: runner.os == 'Linux'
run: python tools/test_terrain_position.py
- name: Verify actual terrain quad geometry and allocation
if: runner.os == 'Linux'
run: python tools/test_quad_reuse.py
- name: Measure actual builder and quad shallow sizes in both header modes
if: runner.os == 'Linux'
run: python tools/test_quad_sizes.py
- name: Verify interrupted DH reads and preserved shared futures
if: runner.os == 'Linux'
run: python tools/test_terrain_read.py
- name: Upload original mod and pack manifests only
uses: actions/upload-artifact@v4
with:
name: horizon-linux
path: |
dist/*.mrpack
dist/horizon-*.jar
dist/worker-pacing-benchmark.txt
dist/debounce-queue-benchmark.txt
dist/idle-queue-benchmark.txt
dist/object-header-benchmark.txt
dist/dispatch-integration.json
dist/tick-dedup-integration.json
dist/culling-empty-integration.json
dist/terrain-read-integration.json
dist/culling-rays-integration.json
dist/terrain-position-integration.json
dist/quad-reuse-integration.json
dist/quad-size-integration.json
dist/server-graphics-smoke.json
dist/client-graphics-smoke.json
dist/client-graphics-macos-title.json
dist/client-graphics-macos-world.json
retention-days: 3
+3 -4
View File
@@ -49,10 +49,6 @@ concurrency:
group: launcher-${{ github.ref == 'refs/heads/main' && 'release' || github.ref }}
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
decide:
runs-on: ubuntu-latest
@@ -192,6 +188,9 @@ jobs:
retry "$launcher" install latest
"$launcher" versions --installed
"$launcher" links --json
"$launcher" menu-style classic
grep -qx 'menuStyle:classic' "$HORIZON_HOME/horizon.txt"
"$launcher" menu-style horizon
clients=("$HORIZON_LAUNCHER_HOME"/versions/v*/horizon-client*)
client="${clients[0]}"
echo "installed client: $client"
-4
View File
@@ -23,10 +23,6 @@ permissions:
concurrency:
group: nightly-audit
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
audit:
runs-on: [self-hosted, horizon-game]
-4
View File
@@ -15,10 +15,6 @@ on:
required: true
permissions:
contents: read
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity:
runs-on: [self-hosted, horizon-game]
+52
View File
@@ -0,0 +1,52 @@
name: promote
# Beta game releases become stable (docs/release-flow.md, "Channels"): every 30 minutes tools/release_channels.py
# promotes the newest beta that has been out 6 hours, passed its release smoke test and isn't covered by an issue
# labelled release-blocker. By hand, from GitHub's app too (Actions > promote > Run workflow):
# gh workflow run promote -f tag=v2026.10.19.03 # that beta now, without the wait
# gh workflow run promote -f tag=v2026.10.19.03 -f force=true # even blocked or without its smoke test
# gh workflow run promote -f dry_run=true # say what would be promoted
# Holding betas back: label an issue release-blocker. While it is open it holds every beta; closed as completed it
# still holds the betas published before it closed. The issue's text is never read (its author could edit it).
# When the oldest waiting beta is 24 hours old, an issue "Promotion to stable has stalled" says so; it closes itself
# after the next promotion.
on:
schedule:
- cron: '13,43 * * * *'
workflow_dispatch:
inputs:
tag:
description: The beta to promote now (empty = the newest beta that is due)
type: string
default: ''
force:
description: Promote that tag even if a release-blocker covers it or its smoke test didn't run
type: boolean
default: false
dry_run:
description: Say what would be promoted, change nothing
type: boolean
default: false
permissions:
contents: write
issues: write
concurrency:
group: promote
cancel-in-progress: false
jobs:
promote:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
# The notes of a promoted release cover every pull request since the last stable one.
fetch-depth: 0
# gh uses GH_TOKEN below; nothing needs git credentials on disk.
persist-credentials: false
- name: Promote
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
FORCE: ${{ inputs.force && 'yes' || '' }}
DRY_RUN: ${{ (inputs.dry_run || github.ref != 'refs/heads/main') && 'yes' || '' }}
run: python3 tools/release_channels.py promote ${TAG:+--tag "$TAG"} ${FORCE:+--force} ${DRY_RUN:+--dry-run}
+15 -38
View File
@@ -1,17 +1,14 @@
name: release
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides:
# a release when 5 pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago, or when the oldest has waited 6 hours, or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them; so does an open issue labelled release-blocker, forced runs too.
# a release when pull requests merged since the last one changed what players run and the newest merged 15 minutes
# ago (the oldest an hour ago, if merges never pause), or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them.
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as the release and GitHub's latest: one
# channel, which launchers, `horizon-server update` and install.sh follow at once. So it is published only after the
# checks that used to gate a promotion pass in this run: the Mac mini's smoke test drew a world, and no release-blocker
# is open (tools/release_train.py gate); otherwise nothing is published. macOS: with the repository variable
# MACOS_SIGN_IDENTITY set, the programs are signed with
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as a beta (a pre-release) that promote.yml makes
# stable after 6 hours. macOS: with the repository variable MACOS_SIGN_IDENTITY set, the programs are signed with
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
# (packaging/macos/README.md); unset (forks), ad hoc.
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
@@ -35,8 +32,6 @@ on:
default: false
permissions:
contents: read
# The open release-blocker issues (decide).
issues: read
concurrency:
group: game-release
cancel-in-progress: false
@@ -133,8 +128,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
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
run: cargo build --release --locked --features horizon-client/mods -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 -p horizon-client --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
@@ -209,7 +203,7 @@ jobs:
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release; the publish job checks it again before it publishes anything.
# Published with the release: a beta becomes stable only with it (tools/release_channels.py).
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
- uses: actions/upload-artifact@v4
if: runner.environment == 'self-hosted'
@@ -256,9 +250,8 @@ jobs:
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
# and with code mods like the client.
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them).
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
@@ -326,7 +319,6 @@ jobs:
timeout-minutes: 15
permissions:
contents: write
issues: read
steps:
- uses: actions/checkout@v4
with:
@@ -349,17 +341,6 @@ jobs:
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test passed, no release-blocker is open)
# Right before publishing: an issue labelled while the builds ran still holds this release. A dry run only
# reports what a real run would do.
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.decide.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
@@ -367,20 +348,16 @@ jobs:
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# The release and the latest at once: there is one channel (docs/release-flow.md). Each archive gets a
# label (file#label), which GitHub's page shows instead of the file name, so nobody takes the game's
# files for the download (#1647). Launchers, servers and install.sh match the name, which stays.
smoke=(); [[ -f dist/smoke-test.json ]] && smoke=(dist/smoke-test.json)
# A beta (pre-release): promote.yml makes it stable and latest after 6 hours (docs/release-flow.md).
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Game files the launcher installs: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
--notes-file notes/body.md --prerelease --latest=false dist/horizon-linux-x86_64.tar.gz \
dist/horizon-windows-x64.zip dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz \
dist/SHA256SUMS "${signature[@]}" "${smoke[@]}"
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }} as the release and latest: $(cat notes/title.txt)"
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }}: $(cat notes/title.txt)"
ls -l dist
- uses: actions/upload-artifact@v4
with:
-4
View File
@@ -11,10 +11,6 @@ permissions:
concurrency:
group: run-parity-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity:
if: github.event.label.name == 'run-parity'
-177
View File
@@ -1,177 +0,0 @@
# Horizon: a map for agents
Minecraft: Java Edition 26.3 rewritten in Rust (wgpu). Vanilla is the specification wherever it has the
feature. Read [CONTRIBUTING.md](CONTRIBUTING.md) once; this page tells you where things are.
Feature → files → harness for the most-changed areas: [docs/architecture.md](docs/architecture.md).
## Rules that bite
- **Never commit Mojang's files**: game jars, assets, decompiled source, prepared resources (`.local/`).
`tools/check_public_tree.py` rejects them; `git config core.hooksPath .githooks` runs it on commit.
- **Keep your runs away from real player data.** Any game or launcher you start gets fresh `mktemp -d`
folders in `HORIZON_HOME` (the game's data), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with a line naming whose agent opened it and with what.
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
contributor's agent names its own person and tool (issues: "Filed by", reviews: "Review by"). Every
comment starts with `🤖 Agent:`. Never write as Ludovic or as anyone else.
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
with that agent's own "Generated with" line. Ludovic's agents use
`Co-Authored-By: Claude <model> <[email protected]>` and
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
they would change the vanilla look.
- Small PRs (< ~500 lines). Labels: `feature` (players get something new), `skip-release` (they see no
change), none for a fix. Agents never add `release-now`: the maintainer decides it. Merges ship in batches of 5, or after
6 hours ([docs/release-flow.md](docs/release-flow.md)).
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
## Crates
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~190 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
| `horizon-assets` | Resource decoding: block-state and item models, baking, atlases |
| `horizon-prepare` | Builds `.local/horizon-client/26.3` from the player's own Minecraft install (runs the Java exporters) |
| `horizon-server` | `horizon-server`: `server.toml`, CLI, supervisor of the headless game ([docs/hosting.md](docs/hosting.md)) |
| `horizon-server-files` | `server.properties`, eula, ops/whitelist/bans, usercache |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
| `horizon-mods`, `horizon-mod-sdk`, `cargo-horizon-mod` | Sandboxed WebAssembly mods: runtime, SDK + WIT, `cargo horizon-mod` ([docs/mods.md](docs/mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
| `horizon-telemetry`, `horizon-telemetry-server`, `horizon-bench` | Opt-in data collection, its ingest server, the benchmark report format |
| `horizon-dualsense`, `horizon-threads`, `horizon-release-sign`, `horizon-bots`, `xtask` | DualSense output, thread priorities, signed release checksums, load-test bots, repo tasks |
| `launcher/` | GPUI launcher: `main.rs` (UI), `backend.rs` (installs, updates), `cli.rs`, `mcp.rs`, `*_page.rs` |
Other top-level folders: `tools/` (harnesses and dev scripts), `tests/` (Python unit tests: `python3 -m
unittest discover -s tests`), `mods/horizon/src/test/java/dev/horizonlab/parity/` (Java oracles),
`website/` (playhorizon.dev; its paths are relative to `website/`), `docs/`, `.github/workflows/`.
## Build and test fast
```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 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
python3 tools/parity.py mob_ai sculk # named harnesses
python3 tools/lan_e2e.py --check chest # two real games over LAN (host in-frame); --server-thread for the host on its server thread
python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
```
- Harnesses and the game need the vanilla data in `.local/` (`minecraft/`, `horizon-client/26.3`, `datagen/`,
`tools/`): see [docs/parity-workflow.md](docs/parity-workflow.md). Without it, pr_ready lists the
harnesses for CI.
- A harness = `tools/test_<name>_parity.py` (324 of them) + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (horizon-world 175, horizon-client 104),
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.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- **macOS:** `cargo build/test --workspace` may fail to link the two wasm guest cdylibs (undefined
`horizon:mods/events` symbols) until #1446 lands: add `--exclude coords-hud --exclude horizon-mods-test-guest`.
Their real check is `cargo test -p horizon-mods` (builds them for `wasm32-unknown-unknown`).
- 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):
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
then `horizon-ctl --socket … state|keys|turn|command|screenshot|quit`
([docs/horizon-ctl-commands.md](docs/horizon-ctl-commands.md)).
## Vanilla reference
- Decompiled sources: `.local/minecraft/26.3/sources/{client,server}/` (`python3 tools/lab.py decompile`).
- Bytecode queries (callers through lambdas, every overload):
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`
([docs/decompilation-status.md](docs/decompilation-status.md)).
- A harness runs the oracle against the real 26.3 jars and the Rust probe on the same inputs, then compares
their JSON. Oracle runs are cached by `tools/parity_shim.py`; `--fresh` reruns them.
- Mirror vanilla's `Math` calls with `horizon_core::jmath`, and vanilla's RNG with `horizon_core::random`:
random draws must happen in vanilla's order.
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
|---|---|
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `window.rs`, `spans.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`, `items.rs`, `item_data.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs` |
| Commands | parse: L: `command/` (`args.rs`, `selector.rs`, `tree_*.rs`, `vanilla.rs`); run: G: `commands.rs` (dispatch) + `commands/*.rs` (`admin`, `execute`, `items`, `players`, `world_cmds`...); L: `scoreboard_cmd.rs`, `data_cmd.rs`, `nbt_path.rs`; G: `functions.rs`, `command_blocks.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs` |
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server` |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `block_entity_nbt.rs`, `options_file.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; L: `keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `osk.rs`, `clipboard.rs`; G: `pad.rs`, `pad_menu.rs` |
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `lang.rs`; crate `horizon-assets`; G: `packs.rs` |
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
| Mods | crate `horizon-mods`; G: `mods.rs`; L: `mod_content.rs`, `mod_list.rs` |
| Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs`, `tweaks.rs`, `options.rs` |
## How `Game` is organised
- `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.
- 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
single player on its server thread), `lan` (`LanHost`: this world is open to the network), `integrated`
(the window's own server thread), `serving` (this `Game` *is* that server thread), `options.dedicated`.
`fn tick` returns `tick_remote()` when `remote` is set; nothing else of the local simulation runs.
- A server's players run through the same gameplay code as the local player: `as_guest` swaps the guest's
state (position, inventory, mode, mining, item use) into `Game` for one call (`game/lan_actions.rs`), and
`levels.rs` swaps whole dimensions in the same way. Changes reach clients through the block log, level
events and the entity tracker (`lan/tracking.rs`, `game/lan_entities.rs`).
- Shared read-only data: `Resources` (`res`: blocks, items, recipes, mob types, biomes) and `PackResources`
(`pack`), both in game.rs.
## The two single-player paths
1. **Server thread (default since #1514):** `game/integrated.rs` runs a second `Game` (`serving`) on a
"Server thread" with one level per dimension; the window joins it as a client through memory (`remote`),
exactly as a LAN guest or a dedicated server's player does. This is what players run. Open
`[server-thread]` issues list the gaps left.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
the world itself, one level that follows the player (`fn enter_dimension`). Most harnesses test this path.
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
when you touch player-facing gameplay:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
python3 tools/lan_e2e.py --check <name> # host in-frame, guest is a server client
python3 tools/lan_e2e.py --server-thread --check <name> # host on its server thread (portals, levels, distance, lava need it)
python3 tools/parity.py dedicated_server server_entities server_player_damage
```
## Docs worth knowing
[docs/parity-workflow.md](docs/parity-workflow.md) (runner, gate, shards, scopes) ·
[docs/native-client.md](docs/native-client.md) (options, controls, horizon-ctl) ·
[docs/server-levels.md](docs/server-levels.md) (levels per dimension) ·
[docs/rust-parity.md](docs/rust-parity.md) (what is ported) · [docs/known-issues.md](docs/known-issues.md) ·
[docs/release-flow.md](docs/release-flow.md) · [docs/README.md](docs/README.md) (index).
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-9
View File
@@ -40,15 +40,6 @@ probe against Horizon, and compares their outputs. There are about 290 of them.
Never commit game files, decompiled source or prepared resources (`.local/`): `tools/check_public_tree.py` rejects
them (`git config core.hooksPath .githooks` runs it before every commit).
## Only original code
Horizon's code is written from Minecraft 26.3 itself and from public documentation, never from another mod, loader
or renderer. Don't read, copy, translate or paraphrase the source of Iris, OptiFine, Sodium or any other project,
and don't send code an AI tool wrote from it. Their user documentation and file formats are fine to read, and their
packs and mods are fine to run and observe. Shader packs were removed on 24 October 2026 (#1561) because their code
had been written from another project's source; they come back only as a clean-room rewrite from a written
description of the pack format.
## Before asking for review
Run the self-check and paste the block it prints into your pull request:
Generated
+399 -61
View File
@@ -155,7 +155,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
"wl-clipboard-rs",
"x11rb",
]
@@ -178,6 +178,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
name = "ascii-canvas"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891"
dependencies = [
"term",
]
[[package]]
name = "ash"
version = "0.38.0+1.3.281"
@@ -205,15 +214,30 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec 0.8.0",
]
[[package]]
name = "bit-set"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
dependencies = [
"bit-vec",
"bit-vec 0.9.1",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bit-vec"
version = "0.9.1"
@@ -259,6 +283,16 @@ dependencies = [
"objc2 0.6.5",
]
[[package]]
name = "borsh"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a"
dependencies = [
"bytes",
"cfg_aliases",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -450,6 +484,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "coords-hud"
version = "0.1.0"
@@ -754,6 +797,41 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.119",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.119",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
@@ -771,6 +849,29 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.119",
"unicode-xid",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -857,6 +958,15 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "ena"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1"
dependencies = [
"log",
]
[[package]]
name = "encoding_rs"
version = "0.8.42"
@@ -1120,6 +1230,58 @@ version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
[[package]]
name = "glsl-lang"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983a18d0c2ecad18ac9ff3c94b069d6bd03b47405a200836d9d8d63def8e34de"
dependencies = [
"glsl-lang-lexer",
"glsl-lang-types",
"lalrpop",
"lalrpop-util",
"lang-util",
"once_cell",
"thiserror 2.0.21",
]
[[package]]
name = "glsl-lang-lexer"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abdbc0bdfbc81d557f4865e695457eb23d68031bfb061fda3444f56511b26eee"
dependencies = [
"glsl-lang-pp",
"glsl-lang-types",
"lalrpop-util",
"lang-util",
"thiserror 2.0.21",
]
[[package]]
name = "glsl-lang-pp"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88677876fb27f116015d0ecebda6d77bdf65f3a6fa3f5dfeed10b8c8f04cb2c5"
dependencies = [
"arrayvec",
"derive_more",
"lang-util",
"string_cache",
"string_cache_codegen",
"thiserror 2.0.21",
]
[[package]]
name = "glsl-lang-types"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8b1532b424d40df634cf2dc2be56d59f994b9ac07fa7c1e3183fd33db9cb07"
dependencies = [
"lang-util",
"thiserror 2.0.21",
]
[[package]]
name = "gpu-allocator"
version = "0.28.0"
@@ -1233,7 +1395,6 @@ dependencies = [
name = "horizon-bench"
version = "0.1.0"
dependencies = [
"horizon-telemetry",
"serde",
"serde_json",
]
@@ -1266,6 +1427,7 @@ dependencies = [
"horizon-mods",
"horizon-protocol",
"horizon-server-files",
"horizon-shaderpack",
"horizon-telemetry",
"horizon-theme",
"horizon-threads",
@@ -1278,9 +1440,9 @@ dependencies = [
"rustix 1.1.5",
"serde",
"serde_json",
"sha1",
"sha2",
"symphonia",
"toml",
"ureq",
"wgpu",
"winit",
@@ -1315,7 +1477,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"socket2 0.5.10",
"socket2",
"spake2",
]
@@ -1330,11 +1492,10 @@ dependencies = [
name = "horizon-mods"
version = "0.1.0"
dependencies = [
"horizon-mods",
"log",
"minisign-verify",
"serde",
"serde_json",
"sha2",
"toml",
"wasmtime",
"wit-component 0.239.0",
@@ -1380,7 +1541,6 @@ dependencies = [
"getrandom 0.3.4",
"horizon-core",
"horizon-world",
"ipconfig",
"serde_json",
"sha1",
"sha2",
@@ -1435,9 +1595,12 @@ dependencies = [
]
[[package]]
name = "horizon-shaders"
name = "horizon-shaderpack"
version = "0.1.0"
dependencies = [
"glsl-lang",
"naga",
"pp-rs",
"zip",
]
@@ -1592,6 +1755,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
@@ -1664,19 +1833,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "ipconfig"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
dependencies = [
"socket2 0.6.5",
"widestring",
"windows-registry",
"windows-result 0.4.1",
"windows-sys 0.61.2",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -1787,6 +1943,73 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
]
[[package]]
name = "lalrpop"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501"
dependencies = [
"ascii-canvas",
"bit-set 0.8.0",
"ena",
"itertools",
"lalrpop-util",
"petgraph 0.7.1",
"pico-args",
"regex",
"regex-syntax",
"sha3",
"string_cache",
"term",
"unicode-xid",
"walkdir",
]
[[package]]
name = "lalrpop-util"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733"
dependencies = [
"regex-automata",
"rustversion",
]
[[package]]
name = "lang-util"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1715a1e56998f119cd6d49ac8a69802dcbe4fca96d8291243d6dae87abeb913"
dependencies = [
"derive_more",
"lalrpop-util",
"lang-util-derive",
"line-span",
"smol_str 0.3.6",
"text-size",
]
[[package]]
name = "lang-util-derive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2acc66aedf0e445534e0a6b3f0f6ee20ef6ebb9647dbe10735e52ea8d6a096"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "lazy_static"
version = "1.5.1"
@@ -1843,6 +2066,12 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "line-span"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29fc123b2f6600099ca18248f69e3ee02b09c4188c0d98e9a690d90dec3e408a"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -1921,12 +2150,6 @@ dependencies = [
"libc",
]
[[package]]
name = "minisign-verify"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1960,7 +2183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
dependencies = [
"arrayvec",
"bit-set",
"bit-set 0.10.0",
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
@@ -1973,6 +2196,7 @@ dependencies = [
"naga-types",
"num-traits",
"once_cell",
"pp-rs",
"rustc-hash 1.1.0",
"spirv",
"thiserror 2.0.21",
@@ -2021,6 +2245,12 @@ dependencies = [
"jni-sys 0.3.1",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.31.3"
@@ -2520,6 +2750,16 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "petgraph"
version = "0.8.3"
@@ -2531,6 +2771,31 @@ dependencies = [
"indexmap",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project"
version = "1.1.13"
@@ -2649,6 +2914,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "pp-rs"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee"
dependencies = [
"unicode-xid",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "presser"
version = "0.3.1"
@@ -2742,6 +3022,15 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
@@ -3080,6 +3369,16 @@ dependencies = [
"digest",
]
[[package]]
name = "sha3"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "shlex"
version = "2.0.1"
@@ -3108,6 +3407,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]]
name = "slab"
version = "0.4.12"
@@ -3157,6 +3462,16 @@ dependencies = [
"serde",
]
[[package]]
name = "smol_str"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
dependencies = [
"borsh",
"serde_core",
]
[[package]]
name = "socket2"
version = "0.5.10"
@@ -3167,16 +3482,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "spake2"
version = "0.4.0"
@@ -3216,6 +3521,37 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "string_cache"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
@@ -3332,6 +3668,15 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "term"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -3341,6 +3686,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -3501,7 +3852,7 @@ checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"petgraph",
"petgraph 0.8.3",
]
[[package]]
@@ -3617,6 +3968,10 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "voxel-kernels"
version = "0.1.0"
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -4218,8 +4573,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
dependencies = [
"arrayvec",
"bit-set",
"bit-vec",
"bit-set 0.10.0",
"bit-vec 0.9.1",
"bitflags 2.13.2",
"bytemuck",
"cfg_aliases",
@@ -4269,7 +4624,7 @@ dependencies = [
"android_system_properties",
"arrayvec",
"ash",
"bit-set",
"bit-set 0.10.0",
"bitflags 2.13.2",
"block2 0.6.2",
"bytemuck",
@@ -4333,12 +4688,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -4471,17 +4820,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result 0.4.1",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -4784,7 +5122,7 @@ dependencies = [
"rustix 0.38.44",
"sctk-adwaita",
"smithay-client-toolkit",
"smol_str",
"smol_str 0.2.2",
"tracing",
"unicode-segmentation",
"wasm-bindgen",
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
exclude = ["launcher", "crates/horizon-telemetry-server", "third_party/wgpu-hal"]
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-shaders", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
resolver = "2"
[workspace.package]
+4 -4
View File
@@ -45,8 +45,8 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Resource packs, shader packs and data packs:** resource packs work, Java shader packs run their full-screen effects (experimental), and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
@@ -56,8 +56,8 @@ Screenshots, captured in the Rust client itself with vanilla assets and no mods
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
<td width="50%"><img src="docs/media/menu-title.webp" alt="Horizon's title screen over a veiled snowy landscape"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="The Video settings in their Simple view: a preset, render distance, field of view, framerate, VSync and GUI scale, each with what it costs"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
+30 -12
View File
@@ -1,20 +1,38 @@
# Third-party notices
## CullLeaves
The optional leaf and mangrove-root face-culling behavior is adapted from
[CullLeaves](https://github.com/TeamMidnightDust/CullLeaves) by MidnightDust.
Its MIT license is reproduced below.
```text
MIT License
Copyright (c) 2020 MidnightDust
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## wgpu-hal
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with one upstream fix applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
## Pixelify Sans
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1.1
(`crates/horizon-theme/fonts/OFL-PixelifySans.txt`).
## Pixelarticons
`crates/horizon-theme/icons/pixel/*.svg` are icons from
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`).
@@ -131,8 +131,8 @@ average) and reached the ThinkPad over the tailnet.
It ran with Paper's defaults except `connection-throttle: -1` in `bukkit.yml`. Every bot connects from one
address, and the default throttle (4 s) refused all but the first. Paperclip patched the pinned server jar
from its cache. Same Java and heap.
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`tools/benchmark-deps/fabric.lock.json`) with Lithium
`mc26.3-0.26.2-fabric` (`tools/benchmark-deps/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`packs/fabric.lock.json`) with Lithium
`mc26.3-0.26.2-fabric` (`packs/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
Same Java and heap.
The Paper and Fabric jars are dev-only downloads into `.local/server-bench/jars` and are never committed or
-2
View File
@@ -8,5 +8,3 @@ description = "The benchmark report format (docs/benchmark-report.md), shared by
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# The hardware probes shared with telemetry (cores, clocks, drive, VRAM, power, form).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
+27 -52
View File
@@ -1,7 +1,8 @@
//! The machine a report was measured on. CPU, RAM and OS are detected here; the GPU and the display
//! come from the program that rendered (the game knows its adapter, the launcher its window).
use horizon_telemetry::system::command_output as output;
use std::process::Command;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@@ -15,22 +16,8 @@ pub struct Hardware {
pub cpu: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_threads: Option<usize>,
/// Physical cores.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_cores: Option<u16>,
/// Base and top boost clock, where the OS gives them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_base_mhz: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_boost_mhz: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ram_mb: Option<u64>,
/// The drive under the game folder: `nvme`, `ssd` or `hdd`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<String>,
/// `laptop` or `desktop`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gpu: Option<Gpu>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -46,9 +33,6 @@ pub struct Gpu {
pub backend: String,
/// `integrated`, `discrete`, `virtual`, `cpu`, `other`.
pub kind: String,
/// Its own memory in MiB (rounded to 256), for a discrete GPU whose driver says.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vram_mb: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@@ -63,50 +47,17 @@ pub struct Display {
impl Hardware {
pub fn detect() -> Hardware {
let probes = probes();
Hardware {
os: std::env::consts::OS.to_owned(),
os_version: os_version(),
arch: std::env::consts::ARCH.to_owned(),
cpu: cpu(),
cpu_threads: std::thread::available_parallelism().ok().map(|n| n.get()),
cpu_cores: probes.cores,
cpu_base_mhz: probes.base,
cpu_boost_mhz: probes.boost,
ram_mb: ram_bytes().map(|b| b >> 20),
storage: None,
form: probes.form,
gpu: None,
display: None,
}
}
/// [`Hardware::detect`] with the kind of drive `game_dir` is on.
pub fn detect_in(game_dir: &std::path::Path) -> Hardware {
Hardware {
storage: horizon_telemetry::system::storage(game_dir).map(str::to_owned),
..Hardware::detect()
}
}
}
/// What the telemetry probes read (`horizon_telemetry::system`).
struct Probes {
cores: Option<u16>,
base: Option<u32>,
boost: Option<u32>,
form: Option<String>,
}
fn probes() -> Probes {
let s = horizon_telemetry::payload::System::detect();
let some = |n: u32| (n > 0).then_some(n);
Probes {
cores: (s.cores > 0).then_some(s.cores),
base: some(s.cpu_base_mhz),
boost: some(s.cpu_boost_mhz),
form: (!s.form.is_empty()).then_some(s.form),
}
}
/// The one-minute load average: above about the number of threads the machine was busy.
@@ -131,7 +82,25 @@ pub fn load_average() -> Option<f64> {
/// `ac` or `battery`; `None` where it can't be read (and on machines without a battery on Linux).
pub fn power_source() -> Option<&'static str> {
horizon_telemetry::system::power()
if cfg!(target_os = "macos") {
let batt = output("pmset", &["-g", "batt"])?;
return Some(if batt.contains("'Battery Power'") {
"battery"
} else {
"ac"
});
}
let mut on_battery = None;
for entry in std::fs::read_dir("/sys/class/power_supply").ok()?.flatten() {
let read =
|name: &str| std::fs::read_to_string(entry.path().join(name)).unwrap_or_default();
match read("type").trim() {
"Mains" if read("online").trim() == "1" => return Some("ac"),
"Battery" => on_battery = Some("battery"),
_ => {}
}
}
on_battery
}
/// Resident memory of a process, in bytes (macOS, Linux).
@@ -172,6 +141,12 @@ pub fn cpu_seconds(pid: u32) -> Option<f64> {
Some(ticks / 100.0)
}
fn output(program: &str, args: &[&str]) -> Option<String> {
let out = Command::new(program).args(args).output().ok()?;
let text = String::from_utf8(out.stdout).ok()?.trim().to_owned();
(out.status.success() && !text.is_empty()).then_some(text)
}
#[cfg(target_os = "macos")]
fn os_version() -> Option<String> {
output("sw_vers", &["-productVersion"])
+1 -7
View File
@@ -245,19 +245,13 @@ pub struct Report {
impl Report {
/// An empty report for this machine, created now.
pub fn new(kind: Kind, app: App) -> Report {
Self::with_hardware(kind, app, Hardware::detect())
}
/// Starts a report using supplied hardware, without running system probes.
/// Interactive apps can collect the remaining machine details after their first frame.
pub fn with_hardware(kind: Kind, app: App, hardware: Hardware) -> Report {
Report {
format: FORMAT.to_owned(),
version: VERSION,
kind,
created: utc_now(),
app,
hardware,
hardware: Hardware::detect(),
conditions: Conditions::default(),
metrics: BTreeMap::new(),
frames: Vec::new(),
+9 -19
View File
@@ -18,8 +18,7 @@ horizon-protocol = { path = "../horizon-protocol" }
horizon-server-files = { path = "../horizon-server-files" }
horizon-friends = { path = "../horizon-friends" }
# 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 }
horizon-mods = { path = "../horizon-mods" }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl"] }
winit = "0.30.13"
pollster = "0.4"
@@ -27,13 +26,13 @@ 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"] }
# Shader packs: pack.toml, and WGSL checked before it reaches wgpu.
toml = "1.1"
naga = { version = "30.0.1", features = ["wgsl-in"] }
# Java (OptiFine/Iris) shader packs, translated to WGSL.
horizon-shaderpack = { path = "../horizon-shaderpack" }
# Sound (ALSA on Linux); the `audio` feature.
cpal = { version = "0.16", optional = true }
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
@@ -43,9 +42,8 @@ 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"] }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
horizon-bench = { path = "../horizon-bench" }
arboard = { version = "3.6.1", default-features = false, features = ["wayland-data-control"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
@@ -77,20 +75,12 @@ default = ["audio", "controllers"]
audio = ["dep:cpal"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense"]
# Runs code mods (wasmtime and cranelift: a quarter of a clean build). On in every release build
# (release.yml); the tests and probes don't need it. Content mods load without it.
mods = ["horizon-mods/runtime"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
# 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"
[lints]
workspace = true
[dev-dependencies]
# 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"] }
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
@@ -1,7 +1,8 @@
//! Replays `tools/test_ambient_sound_parity.py` scripts through Horizon's
//! ambient sound handlers over its `SoundEngine`:
//! `ambient_fixture ROOT INPUT OUTPUT`.
use crate::audio_common as common;
#[path = "audio_common/mod.rs"]
mod common;
use common::{Fake, d, f, flush};
use horizon_client::ambience::{Ambience, AmbientContext, AmbientSounds, Mood, bubble_column_in};
use horizon_client::sound_engine::{Bound, SoundEngine, TickContext, Ticking};
@@ -46,8 +47,8 @@ fn sounds(a: &[&str]) -> AmbientSounds {
}
}
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
engine.listening = true;
let mut backend = Fake::default();
@@ -50,8 +50,8 @@ fn particle(k: ArrowParticle) -> &'static str {
ArrowParticle::Splash => "splash",
}
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
if args.len() != 3 {
return Err("usage: arrow_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into());
}
@@ -18,8 +18,8 @@ fn music(m: Option<&Music>) -> String {
})
}
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let root = std::path::Path::new(&args[1]);
let tables = MusicTables::load(root);
let ambient = AmbientTables::load(root);
@@ -24,8 +24,8 @@ fn db(v: &Value) -> f64 {
f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap())
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let [root, input, output, scene_json] = &args[..] else {
return Err("usage: auto_jump_probe RESOURCES INPUT OUTPUT SCENE_JSON".into());
};
@@ -118,8 +118,8 @@ fn status(s: ShulkerStatus) -> &'static str {
}
}
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: block_entity_anim_fixture RESOURCES INPUTS OUTPUT".into());
};
@@ -110,8 +110,8 @@ fn intern(s: &str) -> &'static str {
}
}
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, textures, output] = args.as_slice() else {
return Err("usage: block_entity_render_fixture RESOURCES INPUTS TEXTURES OUTPUT".into());
};
@@ -26,8 +26,8 @@ fn entity(name: &str) -> BlockEntity {
.unwrap_or_else(|| panic!("no block entity for {name}"))
}
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let input = std::fs::read_to_string(&args[1]).expect("input");
let mut out = String::new();
let mut entities = BlockEntities::default();
@@ -0,0 +1,104 @@
//! Full production fact loading, including file reads, interning and temporary destruction.
//! cargo run --profile fast -p horizon-client --example block_facts_bench -- ROOT [SAMPLES]
//! Use SAMPLES=1 with an OS peak-RSS wrapper; `digest` checks all facts
//! and the shape sharing partition (including sharing between outlines and edges).
use horizon_client::blocks::BlockFacts;
use horizon_world::{DATA_VERSION, nbt::Tag, registry::BlockRegistry};
use serde_json::Value;
use std::{
collections::{HashMap, hash_map::DefaultHasher},
fs,
hash::{Hash, Hasher},
hint::black_box,
path::Path,
time::Instant,
};
type Result<T> = horizon_client::Result<T>;
fn tag(value: &Value) -> Result<Tag> {
Ok(match value {
Value::String(s) => Tag::String(s.as_str().into()),
Value::Number(n) => Tag::Int(i32::try_from(n.as_i64().ok_or("registry number")?)?),
Value::Array(a) => Tag::List(a.iter().map(tag).collect::<Result<_>>()?),
Value::Object(o) => Tag::Compound(
o.iter()
.map(|(k, v)| Ok((k.as_str().into(), tag(v)?)))
.collect::<Result<_>>()?,
),
_ => return Err("registry value".into()),
})
}
fn registry(root: &Path) -> Result<BlockRegistry> {
let facts: Value = serde_json::from_slice(&fs::read(root.join("facts.json"))?)?;
Ok(BlockRegistry::from_nbt(&Tag::compound([
("DataVersion", Tag::Int(DATA_VERSION)),
(
"states",
Tag::List(
facts["states"]
.as_array()
.ok_or("registry states")?
.iter()
.map(|s| tag(&s[4]))
.collect::<Result<_>>()?,
),
),
("defaults", tag(&facts["defaults"])?),
]))?)
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let root = Path::new(
args.first()
.ok_or("usage: block_facts_bench ROOT [SAMPLES|digest]")?,
);
let setup = Instant::now();
let registry = registry(root)?;
println!(
"registry_setup_ms={:.3}",
setup.elapsed().as_secs_f64() * 1000.0
);
if args.get(1).is_some_and(|s| s == "digest") {
let facts = BlockFacts::load(root, &registry)?;
let mut hash = DefaultHasher::new();
let mut shapes = HashMap::new();
facts.air.hash(&mut hash);
for state in &facts.states {
format!("{state:?}").hash(&mut hash);
for shape in [&state.outline, &state.outline_edges] {
let next = shapes.len();
shapes
.entry((shape.as_ptr(), shape.len()))
.or_insert(next)
.hash(&mut hash);
}
}
println!(
"states={} shared_shapes={} digest={:016x}",
facts.states.len(),
shapes.len(),
hash.finish()
);
return Ok(());
}
let samples: usize = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
assert!(samples > 0);
// First load is reported separately; OS file caches are not forcibly evicted.
let load = || -> Result<f64> {
let start = Instant::now();
let facts = black_box(BlockFacts::load(black_box(root), black_box(&registry))?);
let ms = start.elapsed().as_secs_f64() * 1000.0;
drop(facts);
Ok(ms)
};
println!("first_load_ms={:.3}", load()?);
if samples > 1 {
load()?;
load()?;
let mut times = (0..samples).map(|_| load()).collect::<Result<Vec<_>>>()?;
println!("samples_ms={times:.3?}");
times.sort_by(f64::total_cmp);
println!("median_ms={:.3}", times[times.len() / 2]);
}
Ok(())
}
@@ -8,8 +8,8 @@ use std::{
io::{BufRead, BufReader, BufWriter, Write},
path::Path,
};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let root = Path::new(&args[1]);
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
@@ -51,8 +51,8 @@ impl Around for Column {
}
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: block_transform_fixture RESOURCES INPUT OUTPUT")?,
@@ -16,8 +16,8 @@ use std::{
};
/// The stage sprite origin the cracks carry (the oracle compares the geometry and UVs only).
const STAGE: [f32; 2] = [0.25, 0.5];
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let root = Path::new(&args[1]);
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
@@ -323,8 +323,8 @@ fn avatar(c: &Value, out: &mut impl Write) -> Result<()> {
Ok(())
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -70,8 +70,8 @@ fn describe(state: &BlockState) -> String {
format!("{}|{}", state.name, props.join(","))
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: cauldron_fixture RESOURCES INPUT OUTPUT")?,
@@ -82,19 +82,12 @@ pub fn main() -> horizon_client::Result<()> {
let brewing = horizon_client::brewing::Brewing::load(root, &items);
// The inventory menu never reads banner patterns.
let banners = horizon_client::banner::Banners::default();
let station_recipes = horizon_client::stations::StationRecipes::default();
let enchantments = horizon_client::enchant::Enchantments::default();
let names = horizon_client::creative::Names::default();
let env = Env {
items: &items,
recipes: &recipes,
brewing: &brewing,
banners: &banners,
station_recipes: &station_recipes,
enchantments: &enchantments,
names: &names,
trade: None,
maps: None,
};
let mut out = String::new();
for line in input.lines() {
@@ -5,7 +5,6 @@
//! the oracle's JSON form.
//!
//! usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT
use crate::pack_screen_fixture;
use horizon_client::{
chat::{ChatAction, ChatScreen, Commands},
command::vanilla,
@@ -16,11 +15,14 @@ use horizon_client::{
use serde_json::{Value, json};
use std::{fmt::Write as _, fs, path::PathBuf};
use winit::keyboard::KeyCode as K;
#[path = "pack_screen_fixture.rs"]
#[allow(dead_code)]
mod pack_screen_fixture;
#[path = "chat_screen_fixture/registries.rs"]
mod registries;
pub fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
let [root, registries, inputs, output] = args.as_slice() else {
return Err("usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT".into());
};
@@ -0,0 +1,216 @@
//! Resident chunk storage benchmark: generates a block of vanilla terrain, then
//! reports the resident block/biome memory, the cost of lookups, meshing and
//! lighting on it.
//!
//! chunk_bench <client resource dir> [--seed S] [--size N]
#![forbid(unsafe_code)]
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS},
light::{LightTables, apply_lighting, light_chunk},
mesher::{MeshTables, Mesher},
models,
streaming::{ChunkGenerator, Proto},
vanillagen::VanillaTerrain,
};
use horizon_core::biome::BiomeZoom;
use std::{path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let root = PathBuf::from(args.first().ok_or("usage: chunk_bench RESOURCES")?);
let flag = |name: &str, default: i64| -> i64 {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1))
.and_then(|v| v.parse().ok())
.unwrap_or(default)
};
let (seed, size) = (flag("--seed", 12345), flag("--size", 8) as i32);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = MeshTables::new(
&assets,
&render,
&facts,
models::load_visibility(&root, assets.registry)?,
)?;
let light = LightTables::load(&root, assets.registry, &facts)?;
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, seed)?;
let mut worker = terrain.worker();
// Protos for size+4 columns, decorated for the inner size+2, finished as size+2.
let started = Instant::now();
let outer = size + 4;
let mut protos: Vec<Option<Proto>> = (0..outer * outer)
.map(|i| Some(terrain.proto(&mut worker, i % outer, i / outer)))
.collect();
eprintln!("proto: {:.2}s", started.elapsed().as_secs_f64());
let started = Instant::now();
let inner = size + 2;
for z in 1..=inner {
for x in 1..=inner {
let mut region: [Proto; 9] = std::array::from_fn(|k| {
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
protos[((z + dz) * outer + x + dx) as usize]
.take()
.expect("proto")
});
terrain.decorate(&mut worker, &mut region);
for (k, p) in region.into_iter().enumerate() {
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
protos[((z + dz) * outer + x + dx) as usize] = Some(p);
}
}
}
eprintln!("decorate: {:.2}s", started.elapsed().as_secs_f64());
let started = Instant::now();
let mut chunks: Vec<Chunk> = Vec::new();
for z in 1..=inner {
for x in 1..=inner {
let proto = protos[(z * outer + x) as usize].take().expect("proto");
let mut chunk = terrain.finish(proto);
chunk.x = x;
chunk.z = z;
chunks.push(chunk);
}
}
let finish = started.elapsed().as_secs_f64();
println!(
"finish (proto -> resident chunk): {:.1} ms per chunk",
finish * 1e3 / chunks.len() as f64
);
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
// Light all inner-inner columns.
let started = Instant::now();
let mut lit = Vec::new();
for z in 2..inner {
for x in 2..inner {
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let borrowed = horizon_client::mesher::borrow(&region);
lit.push(((x, z), light_chunk(&borrowed, &light, true)));
}
}
println!(
"light: {:.2} ms per column",
started.elapsed().as_secs_f64() * 1e3 / lit.len() as f64
);
let mut chunks: Vec<Chunk> = chunks.iter().map(|c| (**c).clone()).collect();
for ((x, z), sections) in lit {
apply_lighting(&mut chunks[((z - 1) * inner + x - 1) as usize], sections);
}
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
// Memory.
let (mut bytes, mut sections, mut uniform) = (0usize, 0usize, 0usize);
for chunk in &chunks {
bytes += chunk.heap_bytes();
for s in &chunk.sections {
sections += 1;
uniform += usize::from(s.is_uniform(s.get(0)));
}
}
println!(
"resident: {} chunks, {} sections ({} uniform), {:.1} KiB per chunk ({:.2} MiB total)",
chunks.len(),
sections,
uniform,
bytes as f64 / 1024.0 / chunks.len() as f64,
bytes as f64 / 1048576.0
);
// Meshing.
let mut mesher = Mesher {
nether_shading: false,
assets: &assets,
render: &render,
tables: &tables,
biomes: &biomes,
visibility: tables.visibility.clone(),
zoom: BiomeZoom::new(7),
blend_radius: 2,
smooth_lighting: true,
cutout_leaves: true,
cull_leaves: false,
};
let mut best = f64::MAX;
let mut quads = 0usize;
for _ in 0..3 {
let started = Instant::now();
let mut jobs = 0;
quads = 0;
for z in 2..inner {
for x in 2..inner {
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let borrowed = horizon_client::mesher::borrow(&region);
for sy in 0..SECTIONS as i32 {
let mesh = mesher.mesh(&borrowed, sy);
quads += (mesh.solid.len() + mesh.translucent.len()) / 4;
jobs += 1;
}
}
}
best = best.min(started.elapsed().as_secs_f64() / f64::from(jobs));
}
println!(
"mesh: {:.1} us per section ({} quads total)",
best * 1e6,
quads
);
// Random lookups, as collision and entity queries do.
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let started = Instant::now();
let mut sum = 0u64;
let lookups = 20_000_000u64;
for _ in 0..lookups {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let cx = 2 + (state % u64::from((inner - 2) as u32)) as i32;
let cz = 2 + ((state >> 8) % u64::from((inner - 2) as u32)) as i32;
let chunk = &chunks[((cz - 1) * inner + cx - 1) as usize];
let (x, y, z) = (
(state >> 16) as i32 & 15,
MIN_Y + ((state >> 24) % 384) as i32,
(state >> 40) as i32 & 15,
);
sum += u64::from(chunk.get(x, y, z));
}
println!(
"random get: {:.2} ns per lookup (checksum {sum})",
started.elapsed().as_secs_f64() * 1e9 / lookups as f64
);
// Coherent scan, as a column or area query does.
let started = Instant::now();
let mut sum = 0u64;
for _ in 0..4 {
for chunk in &chunks {
for y in MIN_Y..MIN_Y + 384 {
for z in 0..16 {
for x in 0..16 {
sum += u64::from(chunk.get(x, y, z));
}
}
}
}
}
println!(
"scan get: {:.2} ns per lookup (checksum {sum})",
started.elapsed().as_secs_f64() * 1e9 / (4.0 * chunks.len() as f64 * 98304.0)
);
Ok(())
}
@@ -1,8 +1,8 @@
//! CloudRenderer face selection and mesh traversal, without a GPU.
use horizon_client::clouds;
use std::{fmt::Write, fs};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let mut output = String::new();
for (case, line) in fs::read_to_string(&args[1])?.lines().enumerate() {
let values: Vec<i32> = line
@@ -132,8 +132,8 @@ fn tree(d: &Dispatcher, node: usize) -> Value {
v
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("tree") {
let d = &vanilla::registered(false);
let mut v = tree(d, 0);
@@ -24,8 +24,8 @@ fn input_name(input: Input) -> &'static str {
}
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -11,8 +11,8 @@ use horizon_client::{
use serde_json::{Value, json};
use std::{fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT";
let names = Names::load(Path::new(args.get(1).ok_or(usage)?));
let creative = Creative::from_json(&serde_json::from_slice(&fs::read(
@@ -14,8 +14,8 @@ fn d(v: f64) -> String {
format!("{:016x}", v.to_bits())
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in BufReader::new(File::open(&args[1])?).lines().enumerate() {
let c: Value = serde_json::from_str(&line?)?;
@@ -10,8 +10,8 @@ use horizon_client::{
};
use std::{fmt::Write as _, fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: enchanting_fixture RESOURCES INPUT OUTPUT")?,
@@ -3,7 +3,7 @@
//! posed model with `mobs::render::draw` under the case's living-entity root
//! transform and writes each drawn part's packed vertex normals.
//!
//! cargo run --profile fast -p horizon-client --example probe -- entity_anim_fixture \
//! cargo run --profile fast -p horizon-client --example entity_anim_fixture -- \
//! RESOURCES VANILLA.jsonl OUT.jsonl
use horizon_client::mesher::Vertex;
use horizon_client::mobs::{
@@ -288,8 +288,8 @@ fn entity_state(mob: &mut Mob, species: Species, case: &Value) {
}
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let root = PathBuf::from(&args[0]);
let types = MobTypes::load(&root)?;
let animations = MobData::load_animations(&root);
@@ -61,8 +61,8 @@ fn species(id: &str) -> Species {
}
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -53,8 +53,8 @@ fn line(s: &LootStack) -> String {
out
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: entity_death_loot_fixture RESOURCES INPUT OUTPUT")?,
@@ -0,0 +1,140 @@
//! Isolated production mob-model builder benchmark; no assets, Game, GPU or full-frame work.
//! CARGO_TARGET_DIR=/home/bob/Documents/horizon/target CARGO_INCREMENTAL=0 \
//! cargo run --profile fast -p horizon-client --example entity_geometry_bench
use horizon_client::{
mesher::Vertex,
mobs::{Part, Polygon, anim::Poses, render},
};
use std::{hint::black_box, time::Instant};
fn cube(name: String, position: f32) -> Part {
let polygons = (0..6)
.map(|face| {
let axis = face / 2;
let side = (face % 2) as f32;
let mut normal = [0.0; 3];
normal[axis] = side * 2.0 - 1.0;
Polygon {
normal,
vertices: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]].map(|uv| {
let mut vertex = [0.0; 5];
vertex[axis] = side * 8.0;
vertex[(axis + 1) % 3] = uv[0] * 8.0;
vertex[(axis + 2) % 3] = uv[1] * 8.0;
vertex[3] = uv[0];
vertex[4] = uv[1];
vertex
}),
}
})
.collect();
Part {
name,
pose: [position, 0.0, 0.0, 0.1, 0.2, 0.0, 1.0, 1.0, 1.0],
polygons,
children: Vec::new(),
}
}
fn build(model: &Part, poses: &Poses, count: usize, out: &mut Vec<Vertex>) {
for i in 0..count {
render::draw(
black_box(model),
glam::Mat4::from_translation(glam::Vec3::new((i % 32) as f32, 0.0, (i / 32) as f32)),
black_box(poses),
[0.125, 0.25, 0.5, 0.75],
0xffc0_d0e0,
0xf0f0,
0,
out,
);
}
black_box(out.as_slice());
}
fn measure(model: &Part, poses: &Poses, count: usize, iterations: usize, reuse: bool) -> f64 {
let mut retained = Vec::new();
build(model, poses, count, &mut retained); // Untimed warmup.
let start = Instant::now();
for _ in 0..iterations {
let mut out = if reuse {
std::mem::take(&mut retained)
} else {
Vec::new()
};
out.clear();
build(model, poses, count, &mut out);
if reuse {
retained = out;
}
}
start.elapsed().as_secs_f64() * 1e6 / iterations as f64
}
fn main() {
let mut model = cube("body".into(), 0.0);
model.children = (1..6).map(|i| cube(format!("part{i}"), i as f32)).collect();
let poses = Poses::new(&model);
for count in [32, 256, 1024, 4096, 8192] {
let mut expected = Vec::new();
build(&model, &poses, count, &mut expected);
let mut retained = Vec::new();
build(&model, &poses, count, &mut retained);
let (pointer, capacity) = (retained.as_ptr(), retained.capacity());
// Populated -> empty/reset -> smaller -> populated must never leave stale vertices.
for n in [0, count / 2, count, count] {
retained.clear();
build(&model, &poses, n, &mut retained);
let mut fresh = Vec::new();
build(&model, &poses, n, &mut fresh);
assert_eq!(
bytemuck::cast_slice::<Vertex, u8>(&retained),
bytemuck::cast_slice::<Vertex, u8>(&fresh)
);
assert_eq!(
(retained.as_ptr(), retained.capacity()),
(pointer, capacity)
);
}
assert_eq!(
bytemuck::cast_slice::<Vertex, u8>(&retained),
bytemuck::cast_slice::<Vertex, u8>(&expected)
);
let iterations = (100_000 / count).clamp(40, 1000);
let mut fresh = Vec::new();
let mut reused = Vec::new();
let mut paired_speedups = Vec::new();
// Alternate order, reporting medians over eleven warmed batches.
for batch in 0..11 {
let mut pair = [0.0; 2];
for reuse in if batch % 2 == 0 {
[false, true]
} else {
[true, false]
} {
let time = measure(&model, &poses, count, iterations, reuse);
pair[usize::from(reuse)] = time;
if reuse {
reused.push(time);
} else {
fresh.push(time);
}
}
paired_speedups.push(pair[0] / pair[1]);
}
paired_speedups.sort_by(f64::total_cmp);
fresh.sort_by(f64::total_cmp);
reused.sort_by(f64::total_cmp);
println!(
"entities={count} vertices={} capacity={capacity} retained_bytes={} fresh_us={:.3} reused_us={:.3} speedup={:.3}x paired_speedup_median={:.3}x paired_speedup_range={:.3}..{:.3}x iterations={iterations} batches=11 byte_equivalence=pass pointer_capacity_reuse=pass",
retained.len(),
capacity * size_of::<Vertex>(),
fresh[5],
reused[5],
fresh[5] / reused[5],
paired_speedups[5],
paired_speedups[0],
paired_speedups[10]
);
}
}
@@ -13,8 +13,8 @@ fn d(v: f64) -> String {
format!("{:016x}", v.to_bits())
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in BufReader::new(File::open(&args[1])?).lines().enumerate() {
let c: Value = serde_json::from_str(&line?)?;
@@ -9,8 +9,8 @@ use horizon_client::{
};
use std::{collections::HashMap, fmt::Write as _, fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: environment_fixture RESOURCES INPUT OUTPUT")?,
@@ -388,8 +388,8 @@ fn describe(m: &Mob, all: &[Mob], regs: &SpawnRegistries) -> String {
t.join(" ")
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: finalize_spawn_fixture RESOURCES INPUT OUTPUT HALLOWEEN")?,
@@ -11,8 +11,8 @@ use std::{
io::{BufRead, BufReader, BufWriter, Write},
};
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for line in input.lines() {
@@ -1,20 +1,19 @@
//! Firework tooltip companion for tools/test_firework_tooltip_parity.py: the
//! lines `item_tooltip::firework_lines` gives each stack (SNBT, one per line;
//! the `#resources` line names the prepared resources for en_us).
use horizon_client::{enchant::Enchantments, item_data::parse_stack, item_tooltip::firework_lines};
use horizon_client::{item_data::parse_stack, item_tooltip::firework_lines, lang::Language};
use std::{error::Error, fmt::Write, path::PathBuf};
pub fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let input = std::fs::read_to_string(&args[1])?;
let root = input
.lines()
.find_map(|l| l.strip_prefix("#resources "))
.map(PathBuf::from)
.ok_or("no #resources line")?;
// Use the inventory's filtered translations so missing tooltip keys are caught.
let enchantments = Enchantments::load(&root);
let translate = |key: &str| enchantments.translate(key).map(str::to_owned);
let language = Language::load(&[&root], "en_us");
let translate = |key: &str| language.get(key).map(str::to_owned);
let mut output = String::new();
for line in input.lines().filter(|l| !l.starts_with('#')) {
let stack = parse_stack(line)?.ok_or("empty stack")?;
@@ -458,8 +458,8 @@ impl HandWorld for ResetWorld<'_, '_> {
}
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -50,8 +50,8 @@ impl LimiterClock for Script {
}
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -291,8 +291,8 @@ fn key(c: &Value, out: &mut impl Write) -> Result<()> {
Ok(())
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let mut out = BufWriter::new(File::create(&args[1])?);
for line in BufReader::new(File::open(&args[0])?).lines() {
let c: Value = serde_json::from_str(&line?)?;
@@ -13,8 +13,8 @@ use horizon_client::{
use serde_json::Value;
use std::{fmt::Write as _, fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: ghost_recipe_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -2,7 +2,7 @@
use horizon_client::command::args::GameMode;
use horizon_client::items::{ItemStack, take_held_drop};
pub fn main() {
fn main() {
for mode in [
GameMode::Survival,
GameMode::Creative,
@@ -14,22 +14,13 @@ pub fn main() {
for count in [0, 1, 4, 64] {
let mut slot = Some(ItemStack::new("minecraft:stone", count));
let mut dropped = 0;
let mut swings = 0;
let mut packets = 0;
for _ in 0..2 {
if let Some(stack) =
take_held_drop(&mut slot, all, mode == GameMode::Spectator)
{
dropped += stack.count;
swings += 1;
}
if mode != GameMode::Spectator {
packets += 1;
}
println!(
"{} {dropped} {swings} {packets}",
slot.as_ref().map_or(0, |s| s.count),
);
println!("{} {}", slot.as_ref().map_or(0, |s| s.count), dropped);
}
}
}
@@ -10,8 +10,8 @@ use serde_json::Value;
use std::path::Path;
use std::{fmt::Write as _, fs};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: hotbar_fixture RESOURCES CASES OUTPUT";
let items = Items::catalog(Path::new(args.first().and(args.get(1)).ok_or(usage)?))?;
let known = |id: &str| items.get(id).is_some();
@@ -20,8 +20,8 @@ type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[path = "hud_fixture/kinds.rs"]
mod kinds;
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: hud_fixture RESOURCES INPUTS OUTPUT".into());
};
@@ -306,7 +306,6 @@ fn chat(c: &Value, h: i32, atlas: &HudAtlas) -> Vec<GuiDraw> {
height_focused: option("chatHeightFocused"),
height_unfocused: option("chatHeightUnfocused"),
line_spacing: option("chatLineSpacing"),
colors: c["chatColors"].as_bool().unwrap_or(true),
};
for m in c["messages"].as_array().into_iter().flatten() {
// GuiMessageTag indicator colours: systemSinglePlayer and chatError.
@@ -11,8 +11,8 @@ fn number(v: &Value) -> f32 {
fn bits(v: f32) -> u32 {
if v.is_nan() { 0x7fc00000 } else { v.to_bits() }
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let mut out = BufWriter::new(File::create(&args[1])?);
for line in BufReader::new(File::open(&args[0])?).lines() {
let f: Value = serde_json::from_str(&line?)?;
@@ -82,8 +82,8 @@ impl TargetUseWorld for World {
}
}
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
for (i, line) in input.lines().enumerate() {
@@ -0,0 +1,205 @@
//! Compare the exact read/deserialize sequences used by the model loaders.
//! Run with `cargo run -p horizon-client --profile fast --example json_readers_bench`.
//! Optional arguments: resource directory, repetitions (default 9).
use serde_json::{Map, Value, json};
use std::{
collections::BTreeMap,
fmt::Debug,
fs::{self, File},
hint::black_box,
io::BufReader,
path::{Path, PathBuf},
time::Instant,
};
type Result<T> = horizon_client::Result<T>;
#[derive(Clone, Copy)]
enum Reader {
File,
Buffered,
Slice,
}
const READERS: [Reader; 3] = [Reader::File, Reader::Buffered, Reader::Slice];
// These match the map types and reader expressions at all three production sites.
macro_rules! reader {
($name:ident, $map:ty) => {
fn $name(path: &Path, reader: Reader) -> Result<$map> {
Ok(match reader {
Reader::File => serde_json::from_reader(File::open(path)?)?,
Reader::Buffered => serde_json::from_reader(BufReader::new(File::open(path)?))?,
Reader::Slice => serde_json::from_slice(&fs::read(path)?)?,
})
}
};
}
reader!(prepare_facts, Map<String, Value>);
reader!(catalog_facts, BTreeMap<String, Value>);
struct Fixture(PathBuf);
impl Fixture {
fn new() -> Result<Self> {
let root =
std::env::temp_dir().join(format!("horizon-json-readers-{}", std::process::id()));
fs::create_dir(&root)?;
Ok(Self(root))
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn fixtures(root: &Path) -> Result<()> {
let states: Vec<_> = (0..56)
.map(|i| {
json!({"facing": (["north", "east", "south", "west"][i % 4]),
"powered": if i / 4 % 2 == 0 { "false" } else { "true" },
"age": (i / 8).to_string()})
})
.collect();
let mut models = Map::new();
let mut render = Map::new();
for i in 0..1024 {
let name = format!("minecraft:fixture_block_{i:04}");
models.insert(
name.clone(),
json!({
"schema": {"facing": {"named": ["north", "east", "south", "west"]},
"powered": {"named": ["false", "true"]}, "age": {"integer": [0, 6]}},
"states": states
}),
);
render.insert(
name,
json!({"rule": "never", "can_occlude": true, "bars_tag": false}),
);
}
fs::write(root.join("model-states.json"), serde_json::to_vec(&models)?)?;
fs::write(
root.join("render-definitions.json"),
serde_json::to_vec(&render)?,
)?;
Ok(())
}
fn benchmark<T: PartialEq + Debug>(
label: &str,
path: &Path,
repetitions: usize,
read: impl Fn(&Path, Reader) -> Result<T>,
) -> Result<()> {
let expected = read(path, Reader::File)?;
for reader in READERS {
assert_eq!(expected, read(path, reader)?);
for _ in 0..2 {
black_box(read(black_box(path), reader)?);
}
}
drop(expected);
let mut samples: [Vec<f64>; 3] = std::array::from_fn(|_| Vec::new());
for repetition in 0..repetitions {
// Rotate reader order to distribute drift across implementations.
for offset in 0..3 {
let index = (repetition + offset) % 3;
let start = Instant::now();
let value = black_box(read(black_box(path), READERS[index])?);
samples[index].push(start.elapsed().as_secs_f64() * 1000.0);
drop(value); // Match the loaders: retain the parsed tree after loading.
}
}
let median: Vec<_> = samples
.iter_mut()
.map(|s| {
s.sort_by(f64::total_cmp);
s[s.len() / 2]
})
.collect();
println!(
"{label}: {} bytes, {repetitions} samples/reader, median ms: File={:.3}, BufReader={:.3}, read+slice={:.3}; slice speedup={:.2}x",
fs::metadata(path)?.len(),
median[0],
median[1],
median[2],
median[0] / median[2]
);
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let fixture = if args.is_empty() {
Some(Fixture::new()?)
} else {
None
};
let root = fixture
.as_ref()
.map(|f| f.0.clone())
.unwrap_or_else(|| PathBuf::from(&args[0]));
if fixture.is_some() {
fixtures(&root)?;
println!(
"Generated representative fixtures: 1024 blocks, 57344 model states; warm filesystem cache."
);
}
let repetitions = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
assert!(repetitions > 0, "repetitions must be positive");
benchmark(
"prepare model states",
&root.join("model-states.json"),
repetitions,
prepare_facts,
)?;
benchmark(
"catalog model states",
&root.join("model-states.json"),
repetitions,
catalog_facts,
)?;
benchmark(
"render definitions",
&root.join("render-definitions.json"),
repetitions,
catalog_facts,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reader_equivalence_and_errors() -> Result<()> {
let fixture = Fixture::new()?;
let path = fixture.0.join("test.json");
fs::write(&path, br#"{"minecraft:stone":{"schema":{},"states":[{}]},"unicode":"\u2603","duplicate":1,"duplicate":2}"#)?;
let expected = prepare_facts(&path, Reader::File)?;
for reader in READERS {
assert_eq!(expected, prepare_facts(&path, reader)?);
assert_eq!(
expected.iter().collect::<BTreeMap<_, _>>(),
catalog_facts(&path, reader)?.iter().collect()
);
}
for invalid in [b"{\"a\": ".as_slice(), b"{} trailing", b"{\"a\":\"\xff\"}"] {
fs::write(&path, invalid)?;
for reader in READERS {
let error = prepare_facts(&path, reader).unwrap_err();
assert!(error.downcast_ref::<serde_json::Error>().is_some());
assert!(catalog_facts(&path, reader).is_err());
}
}
fs::remove_file(&path)?;
for reader in READERS {
let error = prepare_facts(&path, reader).unwrap_err();
assert_eq!(
error.downcast_ref::<std::io::Error>().unwrap().kind(),
std::io::ErrorKind::NotFound
);
}
Ok(())
}
}
+143
View File
@@ -0,0 +1,143 @@
//! Serve a small flat world on the LAN server without the game, for joining
//! with a real client while developing the protocol:
//! `cargo run --release --example lan_flat -- RESOURCES PORT`.
use horizon_client::{
assets::Assets,
blocks::BlockFacts,
chunk::{Chunk, MIN_Y},
lan::{
Lan,
chunks::ChunkEncoder,
data::SyncData,
hub::{HostEvent, HostState, Hub, HubConfig, TickCtx},
},
world::World,
};
use horizon_protocol::packets::play::Serverbound as S;
use std::{path::PathBuf, sync::Arc, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let root = PathBuf::from(args.next().ok_or("RESOURCES PORT")?);
let port: u16 = args.next().ok_or("port")?.parse()?;
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let state = |name: &str| assets.registry.default_state(name).map_or(0, |s| s.0);
let sync = Arc::new(SyncData::load(&root)?);
let biomes: Vec<String> = sync
.dynamic
.registries
.get("minecraft:worldgen/biome")
.map(|v| v.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default();
let encoder = ChunkEncoder::new(
facts
.states
.iter()
.map(|s| (s.name.clone(), s.fluid_level > 0 || s.waterlogged)),
&sync.builtin,
&biomes,
&biomes,
);
let (stone, dirt, grass) = (
state("minecraft:stone"),
state("minecraft:dirt"),
state("minecraft:grass_block"),
);
let mut world = World::new(facts.air, 0);
for cz in -8..=8 {
for cx in -8..=8 {
let mut chunk = Chunk::empty(cx, cz);
for z in 0..16 {
for x in 0..16 {
for y in MIN_Y..60 {
let v = if y == 59 {
grass
} else if y > 55 {
dirt
} else {
stone
};
chunk.set(x, y, z, v);
}
}
}
chunk.lit = true;
world.chunks.insert((cx, cz), Arc::new(chunk));
}
}
let lan = Lan::start(sync.clone(), port, "Flat - Example".into(), 8)?;
println!("listening on {}", lan.port);
let mut hub = Hub::new(
lan,
sync,
encoder,
HubConfig {
default_game_mode: 1,
guest_commands: true,
force_game_mode: false,
hardcore: false,
view_distance: 8,
simulation_distance: 8,
dimension: "minecraft:overworld".into(),
seed: 0,
max_players: 8,
dedicated: false,
integrated: false,
},
);
let mut next_entity = 100u32;
let host = HostState {
uuid: 42,
name: "Host".into(),
entity_id: 1,
position: [0.5, 60.0, 0.5],
yaw: 0.0,
pitch: 0.0,
head_yaw: 0.0,
on_ground: true,
game_mode: 1,
sneaking: false,
sprinting: false,
equipment: Default::default(),
health: 20.0,
attributes: Vec::new(),
bed: None,
living_flags: 0,
};
loop {
world.game_time += 1;
world.day_time += 1;
let mut ctx = TickCtx {
world: &mut world,
host: Some(&host),
entities: &[],
next_entity: &mut next_entity,
spawn: [8.5, 60.0, 8.5],
difficulty: 2,
difficulty_locked: false,
clock_rate: 1.0,
tick_rate: 20.0,
tick_frozen: false,
tick_steps: (0, 0),
collision: None,
};
for event in hub.tick(&mut ctx) {
match event {
HostEvent::Joined(conn) => hub.place_new_player(conn, &mut ctx),
HostEvent::Packet(_, packet) => {
if std::env::var_os("HORIZON_NET_DEBUG").is_some()
&& !matches!(*packet, S::ClientTickEnd(_))
{
eprintln!("[lan] {packet:?}");
}
}
HostEvent::Chat { line, .. } | HostEvent::Warn(line) | HostEvent::Error(line) => {
println!("{line}")
}
HostEvent::Left(_) => {}
}
}
std::thread::sleep(Duration::from_millis(50));
}
}
@@ -14,8 +14,8 @@ fn unhex(s: &str) -> Vec<u8> {
.collect()
}
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let cases = std::fs::read_to_string(&args[1]).unwrap();
let mut out = String::new();
let mut games: Vec<LanGame> = Vec::new();
@@ -2,7 +2,7 @@
//! server with Horizon's light engine and compare every SkyLight/BlockLight
//! value with the arrays vanilla stored.
//!
//! cargo run --profile fast -p horizon-client --example probe -- light_parity \
//! cargo run --profile fast -p horizon-client --example light_parity -- \
//! RESOURCES DIMENSION_DIR [--nether] [--chunks X0 Z0 X1 Z1]
use horizon_client::{
assets::Assets,
@@ -21,8 +21,8 @@ use std::{collections::HashMap, path::PathBuf};
type Arrays = HashMap<i32, (Option<Vec<u8>>, Option<Vec<u8>>)>;
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let root = PathBuf::from(
args.first()
.ok_or("usage: light_parity RESOURCES DIMENSION_DIR")?,
@@ -22,8 +22,8 @@ use std::{
sync::Arc,
};
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(".local/horizon-client/26.3");
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
@@ -11,41 +11,8 @@ use horizon_client::{
items::Items,
menu::{Env, Input, Kind, Menu},
};
use horizon_protocol::registry::Ids;
use serde_json::Value;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
fs,
path::Path,
};
/// The case's maps: (locked, scale) by id, and the next free id.
#[derive(Default)]
struct CaseMaps {
maps: RefCell<HashMap<i32, (bool, u8)>>,
next: Cell<i32>,
}
impl horizon_client::menu::Maps for CaseMaps {
fn state(&self, id: i32) -> Option<(bool, u8)> {
self.maps.borrow().get(&id).copied()
}
/// `MapItemSavedData.scaled` (unlocked, one scale up) or `locked`.
fn derive(&self, id: i32, scale: bool) -> Option<i32> {
let (_, s) = self.state(id)?;
let new = self.next.get();
self.next.set(new + 1);
let made = if scale {
(false, (s + 1).min(4))
} else {
(true, s)
};
self.maps.borrow_mut().insert(new, made);
Some(new)
}
}
use std::{fs, path::Path};
fn state(menu: &Menu) -> String {
let slots: Vec<String> = (0..menu.slot_count())
@@ -57,26 +24,17 @@ fn state(menu: &Menu) -> String {
.iter()
.map(|(slot, stack)| format!("{slot}:{}", canon_stack(stack.as_ref())))
.collect();
let line = format!(
"{} | {} |{data} | {} | {} | {}",
format!(
"{} | {} |{data} | {} | {}",
slots.join(" "),
canon_stack(menu.carried.as_ref()),
menu.drops.join(";"),
equipped.join(";"),
menu.player_sounds.join(";")
);
if menu.kind() != Kind::Lectern {
return line;
}
// No inventory slots: the inventory Take Book fills.
let items: String = (0..36)
.map(|i| format!(" {}", canon_stack(menu.player_item(i))))
.collect();
format!("{line} | inv{items}")
equipped.join(";")
)
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: menu_fixture RESOURCES INPUT OUTPUT")?,
@@ -86,28 +44,13 @@ pub fn main() -> horizon_client::Result<()> {
let recipes = Recipes::load(root, &items)?;
let brewing = horizon_client::brewing::Brewing::load(root, &items);
let banners = horizon_client::banner::Banners::load(root);
let station_recipes = horizon_client::stations::StationRecipes::load(root, &items);
let enchantments = horizon_client::enchant::Enchantments::load(root);
let names = horizon_client::creative::Names::load(root);
let case_maps = CaseMaps::default();
let env = Env {
items: &items,
recipes: &recipes,
brewing: &brewing,
banners: &banners,
station_recipes: &station_recipes,
enchantments: &enchantments,
names: &names,
trade: None,
maps: Some(&case_maps),
};
// The mob effects' network ids (BeaconMenu.encodeEffect).
let builtin = horizon_protocol::registry::Builtin::find(root)
.and_then(|p| horizon_protocol::registry::Builtin::load(&p).ok());
let effects = builtin
.as_ref()
.and_then(|b| b.get("minecraft:mob_effect"))
.cloned();
let mut out = String::new();
for line in input.lines() {
let case: Value = serde_json::from_str(line)?;
@@ -125,41 +68,15 @@ pub fn main() -> horizon_client::Result<()> {
.unwrap_or(name),
)
.ok_or("menu kind")?;
// [id, scale, locked] each; the level's new maps from 1000.
case_maps.next.set(1000);
*case_maps.maps.borrow_mut() = case["maps"]
.as_array()
.into_iter()
.flatten()
.map(|m| {
let id = m[0].as_i64().unwrap_or(0) as i32;
(
id,
(
m[2].as_bool().unwrap_or(false),
m[1].as_u64().unwrap_or(0) as u8,
),
)
})
.collect();
let mut menu = Menu::new(kind);
menu.creative = case["creative"].as_bool().unwrap_or(false);
menu.levels = case["levels"].as_i64().unwrap_or(0) as i32;
for (i, slot) in case["slots"].as_array().ok_or("slots")?.iter().enumerate() {
menu.set_slot(&env, i, parse_stack(slot.as_str().unwrap_or("-"))?);
}
menu.carried = parse_stack(case["carried"].as_str().unwrap_or("-"))?;
for (i, item) in case["inventory"]
.as_array()
.into_iter()
.flatten()
.enumerate()
{
menu.set_player_item(i, parse_stack(item.as_str().unwrap_or("-"))?);
}
menu.settle(&env);
for (i, v) in case["data"].as_array().ok_or("data")?.iter().enumerate() {
menu.set_data(i, v.as_i64().unwrap_or(0) as i32);
menu.data[i] = v.as_i64().unwrap_or(0) as i32;
}
let mut furnace = furnace_kind.map(|kind| Furnace {
kind,
@@ -172,7 +89,6 @@ pub fn main() -> horizon_client::Result<()> {
});
menu.drops.clear();
menu.equipped.clear();
menu.player_sounds.clear();
out += &format!("init {}\n", state(&menu));
for op in case["ops"].as_array().ok_or("ops")? {
let slot = op[0].as_i64().unwrap_or(0) as i32;
@@ -184,26 +100,6 @@ pub fn main() -> horizon_client::Result<()> {
out += &format!("{}\n", state(&menu));
continue;
}
// ServerboundRenameItemPacket: AnvilMenu.setItemName.
"RENAME" => {
menu.rename(&env, op[3].as_str().unwrap_or_default());
out += &format!("{}\n", state(&menu));
continue;
}
// ServerboundSetBeaconPacket: BeaconMenu.updateEffects.
"SET_BEACON" => {
fn power<'a>(ids: Option<&Ids>, v: &'a Value) -> Option<(i32, &'a str)> {
let name = v.as_str()?;
Some((ids?.id(name)?, name))
}
let (primary, secondary) = (
power(effects.as_ref(), &op[3]),
power(effects.as_ref(), &op[4]),
);
menu.beacon_update(&env, primary, secondary);
out += &format!("{}\n", state(&menu));
continue;
}
// ServerboundContainerSlotStateChangedPacket.
"SLOT_STATE" => {
if (0..9).contains(&slot) {
@@ -4,8 +4,8 @@
//! rows like VanillaMipmapOracle.
use std::{fmt::Write as _, fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let input = fs::read_to_string(&args[0])?;
let mut lines = input.lines();
let root = Path::new(lines.next().ok_or("no resources root")?);
@@ -293,47 +293,6 @@ fn spawn_seed(index: usize) -> i64 {
1000 + 7919 * index as i64
}
/// `Villager.spawnGolemIfNeeded` for villager `a`, the game's way: the golem
/// draws from the level's random and joins the scenario's mobs next tick,
/// seeded as the oracle seeds what a tick spawned.
fn summon_golem(
mobs: &mut [Mob],
a: usize,
needed: usize,
w: &MobWorld,
types: &MobTypes,
born: &mut Vec<Mob>,
level_random: &mut LegacyRandom,
) {
use horizon_client::mobs::{villager, villager_brain};
let Some(near) = villager::golem_summoners(mobs, a, w.game_time, needed) else {
return;
};
let kind = Arc::new(
types
.types
.get("minecraft:iron_golem")
.expect("iron golem")
.clone(),
);
let index = mobs.len() + born.len();
let p = mobs[a].position;
let start = [p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32];
let golem = villager::summon_golem(w, start, level_random, |random, at, yaw| {
let mut m = Mob::new(100_000 + index as u32, kind.clone(), at, yaw);
villager::finalize_summoned_golem(&mut m, random);
m
});
let Some(mut golem) = golem else {
return;
};
golem.random = LegacyRandom::new(spawn_seed(index));
born.push(golem);
for j in near {
villager_brain::golem_detected(&mut mobs[j], w.game_time);
}
}
/// Villager events that act at once in vanilla (`TradeWithVillager` and
/// `VillagerMakeLove` write the other villager directly).
fn villager_events(
@@ -345,7 +304,6 @@ fn villager_events(
items: &Items,
born: &mut Vec<Mob>,
day_time: i64,
level_random: &mut LegacyRandom,
) {
use horizon_client::mobs::villager;
let index = |mobs: &[Mob], id: u32| {
@@ -356,7 +314,6 @@ fn villager_events(
while i < events.len() {
match events[i] {
MobEvent::VillagerMeet { mob, other } => {
events.remove(i);
if let (Some(a), Some(b)) = (index(mobs, mob), index(mobs, other)) {
let meeting = villager::meet(mobs, a, b, w.game_time, |item| {
items.get(item).map_or(64, |i| i.max_stack)
@@ -365,16 +322,8 @@ fn villager_events(
meeting.thrown.is_empty(),
"villagers toss items: not modelled here"
);
if meeting.gossiped {
summon_golem(mobs, a, 5, w, types, born, level_random);
}
}
}
MobEvent::GolemCheck { mob, needed } => {
events.remove(i);
if let Some(a) = index(mobs, mob) {
summon_golem(mobs, a, needed, w, types, born, level_random);
}
}
MobEvent::VillagerBirth {
parent,
@@ -810,8 +759,8 @@ fn villager(m: &Mob) -> String {
s
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() != 3 {
return Err("usage: mob_ai_probe RESOURCES SCENARIOS OUTPUT".into());
}
@@ -1078,7 +1027,6 @@ pub fn main() -> horizon_client::Result<()> {
nearest_player: player.as_ref().filter(|p| !p.spectator).map(|p| p.position),
others: $snapshot,
others_grid: $grid,
others_index: None,
daylight: (world.day_time.rem_euclid(24000)) < 12000,
game_time: world.game_time,
day_time: world.day_time,
@@ -1184,7 +1132,7 @@ pub fn main() -> horizon_client::Result<()> {
}
#[allow(unused_mut)] // filled only with the rng-trace feature
let mut traces: Vec<String> = Vec::new();
let mut pushes = horizon_client::mobs::PendingPushes::default();
let mut pushes = Vec::new();
let mut born = Vec::new();
for i in 0..mobs.len() {
if mobs[i].removed {
@@ -1218,7 +1166,6 @@ pub fn main() -> horizon_client::Result<()> {
&items,
&mut born,
s.time,
&mut scratch,
);
horizon_client::mobs::warden::hear_mob_events(&mut mobs, &events, from, &w);
// Melee hits on other mobs land during the attacker's tick
@@ -1375,31 +1322,6 @@ pub fn main() -> horizon_client::Result<()> {
world.set(at[0], at[1], at[2], registry.resolve(&st)?)?;
}
}
// Doors villagers opened or closed (DoorBlock.setOpen), both halves.
let doors: Vec<([i32; 3], bool)> = events
.iter()
.filter_map(|e| match e {
MobEvent::SetDoorOpen { pos, open } => Some((*pos, *open)),
_ => None,
})
.collect();
for (at, open) in doors {
let Some(half) = registry
.state(world.block(at[0], at[1], at[2]))
.and_then(|s| s.properties.get("half").cloned())
else {
continue;
};
let other = if half == "lower" { 1 } else { -1 };
for y in [at[1], at[1] + other] {
if let Some(mut st) = registry.state(world.block(at[0], y, at[2])).cloned()
&& st.properties.contains_key("open")
{
st.properties.insert("open".to_owned(), open.to_string());
world.set(at[0], y, at[2], registry.resolve(&st)?)?;
}
}
}
mobs.append(&mut born);
mobs.append(&mut added);
for e in &heart_events {
@@ -6,8 +6,8 @@ use horizon_client::mobs::{Mob, MobTypes};
use horizon_core::geometry::Vec3;
use std::{fmt::Write as _, fs, path::Path, sync::Arc};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: mob_attributes_fixture RESOURCES OUTPUT")?,
@@ -1,6 +1,7 @@
//! Replays `tools/test_music_parity.py` scripts through Horizon's
//! `MusicManager` over its `SoundEngine`: `music_fixture ROOT INPUT OUTPUT`.
use crate::audio_common as common;
#[path = "audio_common/mod.rs"]
mod common;
use common::{Fake, describe, f, flush};
use horizon_client::music::{Music, MusicFrequency, MusicManager};
use horizon_client::sound_engine::{Bound, MUSIC, SoundEngine, TickContext};
@@ -20,8 +21,8 @@ fn music(spec: &str) -> Option<Music> {
))
}
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
engine.listening = true;
let mut backend = Fake::default();
@@ -111,8 +111,6 @@ struct World<'a> {
slime: f32,
starts: Vec<Start>,
players: Vec<([f64; 3], String)>,
/// Where the non-spectators among them are.
nearest: Vec<[f64; 3]>,
respawn: Option<[i32; 3]>,
unloaded: HashSet<(i32, i32)>,
spawn_mobs: bool,
@@ -304,8 +302,19 @@ impl NaturalLevel for World<'_> {
fn structure_spawns(&self) -> &StructureSpawns {
self.s.structures
}
fn nearest_player_positions(&self) -> &[[f64; 3]] {
&self.nearest
fn nearest_player_distance_sqr(&self, x: f64, y: f64, z: f64) -> Option<f64> {
let mut best: Option<f64> = None;
for (p, mode) in &self.players {
if mode == "spectator" {
continue;
}
let d = [p[0] - x, p[1] - y, p[2] - z];
let d = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
if best.is_none_or(|b| d < b) {
best = Some(d);
}
}
best
}
fn respawn(&self) -> Option<[i32; 3]> {
self.respawn
@@ -508,8 +517,8 @@ fn yaw_text(entity: &str, v: f32) -> String {
}
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: natural_spawn_fixture RESOURCES INPUT OUTPUT")?,
@@ -721,11 +730,6 @@ pub fn main() -> horizon_client::Result<()> {
dimension_id: dimension_id.clone(),
slime: get("slime", "0.0").parse()?,
starts,
nearest: players
.iter()
.filter(|(_, mode)| mode != "spectator")
.map(|(p, _)| *p)
.collect(),
players,
respawn,
unloaded,
@@ -7,8 +7,8 @@ use horizon_client::{
};
use std::{fs, io::Write};
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let mut names: Vec<_> = fs::read_dir(&args[0])?
.map(|e| e.map(|e| e.path()))
.collect::<std::io::Result<_>>()?;
@@ -30,8 +30,8 @@ fn types(root: &std::path::Path, file: PathBuf, key: &str) -> String {
})
.collect()
}
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let input = fs::read_to_string(&args[1])?;
let mut lines = input.lines();
let root: Vec<&str> = lines.next().ok_or("empty input")?.split(' ').collect();
@@ -5,7 +5,7 @@
//!
//! usage: pack_screen_fixture RESOURCES CASES OUTPUT
use horizon_client::{
hud::{HudAtlas, PACK_ICON_SIZE},
hud::HudAtlas,
lang,
pack_screen::{Compat, PackInfo, PackScreen, Source},
packs::{self, Position},
@@ -13,7 +13,7 @@ use horizon_client::{
text::Text,
};
use serde_json::{Value, json};
use std::{fmt::Write as _, fs, path::PathBuf, sync::Arc};
use std::{fmt::Write as _, fs, path::PathBuf};
/// Styled runs: [text, rgb or null, bold, italic, underlined, strikethrough, obfuscated].
pub fn runs(t: &Text) -> Value {
@@ -66,8 +66,8 @@ pub fn encode(draws: &[Draw]) -> Value {
)
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: pack_screen_fixture RESOURCES CASES OUTPUT".into());
};
@@ -105,9 +105,7 @@ pub fn main() -> horizon_client::Result<()> {
Some("unknown") => Compat::Unknown,
_ => Compat::Compatible,
},
icon: p["icon"].as_bool().unwrap_or(false).then(|| {
Arc::from(vec![0; (PACK_ICON_SIZE * PACK_ICON_SIZE * 4) as usize])
}),
icon: p["icon"].as_bool().unwrap_or(false),
required: p["required"].as_bool().unwrap_or(false),
position: if p["position"] == "top" {
Position::Top
@@ -35,8 +35,8 @@ fn lists(m: &SelectionModel) -> Value {
json!({"unselected": side(false), "selected": side(true)})
}
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
let [inputs, output] = args.as_slice() else {
return Err("usage: pack_selection_fixture INPUTS OUTPUT".into());
};
@@ -140,8 +140,8 @@ fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Resul
Ok(world)
}
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = &Path::new(".local/horizon-client").join(horizon_core::VANILLA_VERSION);
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
@@ -78,8 +78,8 @@ fn case(line: &str) -> Option<String> {
Some(out)
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = fs::read_to_string(
args.get(1)
.ok_or("usage: pick_block_data_fixture INPUT OUTPUT")?,
@@ -28,8 +28,8 @@ fn state(menu: &Menu) -> String {
)
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: place_recipe_fixture RESOURCES CASES ORDERS OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -38,19 +38,12 @@ pub fn main() -> horizon_client::Result<()> {
let recipes = Recipes::load(root, &items)?;
let brewing = horizon_client::brewing::Brewing::load(root, &items);
let banners = horizon_client::banner::Banners::default();
let station_recipes = horizon_client::stations::StationRecipes::default();
let enchantments = horizon_client::enchant::Enchantments::default();
let names = horizon_client::creative::Names::default();
let env = Env {
items: &items,
recipes: &recipes,
brewing: &brewing,
banners: &banners,
station_recipes: &station_recipes,
enchantments: &enchantments,
names: &names,
trade: None,
maps: None,
};
let mut out = String::new();
for (line, order) in cases.lines().zip(orders.lines()) {
@@ -8,8 +8,8 @@ fn number(value: &Value) -> f64 {
f64::from_bits(u64::from_str_radix(value.as_str().unwrap(), 16).unwrap())
}
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
for line in input.lines() {
@@ -1,49 +0,0 @@
//! How the probe binary runs a probe. main.rs lists the probes and nothing else (tools/parity.py leaves it out of
//! the harnesses' dependencies); code every probe relies on goes here.
use std::process::ExitCode;
/// `probes! { a, b, }` declares the binary's `main`, which runs the probe module named by its first argument.
macro_rules! probes {
($($name:ident,)*) => {
fn run(name: &str) -> Option<std::process::ExitCode> {
use std::process::Termination;
Some(match name {
$(stringify!($name) => $name::main().report(),)*
_ => return None,
})
}
fn main() -> std::process::ExitCode {
dispatch::main(&[$(stringify!($name)),*], run)
}
};
}
/// The probe's command line as its own example saw it: the program, then the arguments after the probe's name.
pub fn args() -> std::vec::IntoIter<String> {
let mut args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
args.remove(1);
}
args.into_iter()
}
/// args(), as OsStrings.
pub fn args_os() -> std::vec::IntoIter<std::ffi::OsString> {
let mut args: Vec<_> = std::env::args_os().collect();
if args.len() > 1 {
args.remove(1);
}
args.into_iter()
}
pub fn main(names: &[&str], run: fn(&str) -> Option<ExitCode>) -> ExitCode {
let name = std::env::args().nth(1).unwrap_or_default();
run(&name).unwrap_or_else(|| {
eprintln!(
"usage: client_probe <name> [arguments...]\nprobes: {}",
names.join(" ")
);
ExitCode::from(2)
})
}
@@ -1,178 +0,0 @@
//! Every horizon-client parity probe in one binary: `probe <name> [arguments]` runs `<name>.rs` as its own
//! example did (tools/test_*_parity.py, tools/oracle_harness.py, tools/audio_harness.py). Linking ~80 examples,
//! each with the whole client in it, was most of the parity job's probe build; one binary builds it once.
//!
//! 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.
#[macro_use]
mod dispatch;
use dispatch::{args, args_os};
mod ambient_fixture;
mod arrow_probe;
mod audio_attributes_fixture;
/// Sound-engine fakes the audio probes share.
mod audio_common;
mod auto_jump_probe;
mod block_entity_anim_fixture;
mod block_entity_render_fixture;
mod block_entity_tickers_fixture;
mod block_particles_fixture;
mod block_transform_fixture;
mod breaking_fixture;
mod camera_probe;
mod cauldron_fixture;
mod chat_screen_fixture;
mod cloud_fixture;
mod command_parse_fixture;
mod container_screen_probe;
mod creative_search_fixture;
mod emitter_particles_probe;
mod enchanting_fixture;
mod entity_anim_fixture;
mod entity_components_probe;
mod entity_death_loot_fixture;
mod entity_particles_probe;
mod environment_fixture;
mod finalize_spawn_fixture;
mod fire_starter_sound_probe;
mod firework_tooltip_fixture;
mod first_person_probe;
mod frame_limiter_probe;
mod game_loop_probe;
mod ghost_recipe_fixture;
mod held_drop_fixture;
mod hotbar_fixture;
mod hud_fixture;
mod input_probe;
mod interaction_input_fixture;
mod lan_probe;
mod light_parity;
mod marker_particles_probe;
mod menu_fixture;
mod mipmap_probe;
mod mob_ai_probe;
mod mob_attributes_fixture;
mod music_fixture;
mod natural_spawn_fixture;
mod options_probe;
mod pack_resources_fixture;
mod pack_screen_fixture;
mod pack_selection_fixture;
mod particle_providers_probe;
mod pick_block_data_fixture;
mod place_recipe_fixture;
mod player_sync_fixture;
mod raid_fixture;
mod random_tick_sections_probe;
mod recipe_book_fixture;
mod recipe_book_render_fixture;
mod recipe_book_view_fixture;
mod recipe_fixture;
mod scene_collision_probe;
mod scene_motion_probe;
mod scoreboard_fixture;
mod screen_layout_fixture;
mod section_mesh_fixture;
mod server_files_probe;
mod situational_music_fixture;
mod skin_image_fixture;
mod sound_engine_fixture;
mod sound_event_fixture;
mod spawn_rules_fixture;
mod sprint_probe;
mod subtitle_fixture;
mod tab_list_fixture;
mod team_rules_fixture;
mod toggle_keys_probe;
mod tooltip_display_fixture;
mod trading_fixture;
mod translucency_fixture;
mod weather_fixture;
mod world_border_render_fixture;
mod world_options_fixture;
probes! {
ambient_fixture,
arrow_probe,
audio_attributes_fixture,
auto_jump_probe,
block_entity_anim_fixture,
block_entity_render_fixture,
block_entity_tickers_fixture,
block_particles_fixture,
block_transform_fixture,
breaking_fixture,
camera_probe,
cauldron_fixture,
chat_screen_fixture,
cloud_fixture,
command_parse_fixture,
container_screen_probe,
creative_search_fixture,
emitter_particles_probe,
enchanting_fixture,
entity_anim_fixture,
entity_components_probe,
entity_death_loot_fixture,
entity_particles_probe,
environment_fixture,
finalize_spawn_fixture,
fire_starter_sound_probe,
firework_tooltip_fixture,
first_person_probe,
frame_limiter_probe,
game_loop_probe,
ghost_recipe_fixture,
held_drop_fixture,
hotbar_fixture,
hud_fixture,
input_probe,
interaction_input_fixture,
lan_probe,
light_parity,
marker_particles_probe,
menu_fixture,
mipmap_probe,
mob_ai_probe,
mob_attributes_fixture,
music_fixture,
natural_spawn_fixture,
options_probe,
pack_resources_fixture,
pack_screen_fixture,
pack_selection_fixture,
particle_providers_probe,
pick_block_data_fixture,
place_recipe_fixture,
player_sync_fixture,
raid_fixture,
random_tick_sections_probe,
recipe_book_fixture,
recipe_book_render_fixture,
recipe_book_view_fixture,
recipe_fixture,
scene_collision_probe,
scene_motion_probe,
scoreboard_fixture,
screen_layout_fixture,
section_mesh_fixture,
server_files_probe,
situational_music_fixture,
skin_image_fixture,
sound_engine_fixture,
sound_event_fixture,
spawn_rules_fixture,
sprint_probe,
subtitle_fixture,
tab_list_fixture,
team_rules_fixture,
toggle_keys_probe,
tooltip_display_fixture,
trading_fixture,
translucency_fixture,
weather_fixture,
world_border_render_fixture,
world_options_fixture,
}
@@ -1,132 +0,0 @@
//! Random-tick sections companion for tools/test_random_tick_sections_parity.py:
//! the game's `RandomTickSections` choosing the sections of a chunk that
//! `Behaviours::tick_chunk_random` ticks, after the case's edits (see
//! VanillaRandomTickSectionsOracle for the format; the `#resources` line names
//! the prepared resources). The level the ticks run against reads as air
//! everywhere, so nothing ticks for real: its reads are the drawn positions.
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, Section},
random_tick_sections::RandomTickSections,
};
use horizon_core::{
position::{BlockPos, ChunkPos},
random::LegacyRandom,
};
use horizon_world::{
fluid::FluidKind,
level::{GameRules, LevelAccess, LevelEvent, Ticks, data},
registry::BlockStateId,
};
use std::{cell::RefCell, error::Error, fmt::Write, path::PathBuf, sync::Arc};
struct Air {
reads: RefCell<Vec<BlockPos>>,
random: LegacyRandom,
speed: i32,
}
impl LevelAccess for Air {
fn block_state(&self, pos: BlockPos) -> BlockStateId {
self.reads.borrow_mut().push(pos);
BlockStateId(0)
}
fn set_raw(&mut self, _: BlockPos, _: BlockStateId) -> Option<BlockStateId> {
None
}
fn is_loaded(&self, _: BlockPos) -> bool {
true
}
fn random(&mut self) -> &mut LegacyRandom {
&mut self.random
}
fn game_time(&self) -> i64 {
0
}
fn game_rules(&self) -> GameRules {
GameRules {
random_tick_speed: self.speed,
..Default::default()
}
}
fn emit(&mut self, _: LevelEvent) {}
}
pub fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = crate::args().collect();
let input = std::fs::read_to_string(&args[1])?;
let root = input
.lines()
.find_map(|l| l.strip_prefix("#resources "))
.map(PathBuf::from)
.ok_or("no #resources line")?;
let registry = data::load_registry(&root)?;
let (facts, behaviours) = BlockFacts::load_pair(&root, &registry)?;
let mut output = String::new();
for line in input.lines().filter(|l| !l.starts_with('#')) {
let mut words = line.split(' ');
let mut ticks = Ticks::new(0);
ticks.random_tick_value = words.next().ok_or("no randValue")?.parse()?;
let mut level = Air {
reads: RefCell::default(),
random: LegacyRandom::new(0),
speed: words.next().ok_or("no speed")?.parse()?,
};
let mut chunk = Arc::new(Chunk::empty(0, 0));
let mut cache = RandomTickSections::default();
for word in words {
let op: Vec<&str> = word.split(':').collect();
let int = |i: usize| -> Result<i32, Box<dyn Error>> { Ok(op[i].parse()?) };
match op[0] {
"s" => {
let (section, index) = (int(1)?, int(2)?);
Arc::make_mut(&mut chunk).set(
index & 15,
MIN_Y + section * 16 + (index >> 8),
index >> 4 & 15,
int(3)? as u32,
);
}
"f" => {
let (a, b, m) = (int(2)? as u32, int(3)? as u32, int(4)? as usize);
let values = std::array::from_fn(|i| if i % m == m - 1 { b } else { a });
Arc::make_mut(&mut Arc::make_mut(&mut chunk).sections[int(1)? as usize])
.blocks = Section::from_blocks(&values);
}
"t" => {
output.push('|');
level.reads.borrow_mut().clear();
behaviours.tick_chunk_random(
&mut level,
&mut ticks,
ChunkPos::new(0, 0),
|_, from| cache.get((0, 0), &chunk, &facts).from(from),
);
for pos in level.reads.borrow().iter() {
let state = chunk.block(pos.x, pos.y, pos.z);
let fluid = matches!(
behaviours.fluid(state).kind(),
FluidKind::Lava | FluidKind::FlowingLava
);
write!(
output,
" {},{},{},{}{}",
pos.x,
pos.y,
pos.z,
if behaviours.is_randomly_ticking(state) {
'B'
} else {
'-'
},
if fluid { 'F' } else { '-' },
)?;
}
}
other => return Err(format!("unknown op {other}").into()),
}
}
writeln!(output, " | {}", ticks.random_tick_value)?;
}
std::fs::write(&args[2], output)?;
Ok(())
}
@@ -459,8 +459,8 @@ fn run_codec(c: &BTreeMap<&str, &str>) -> Result<String, String> {
Ok(log.join(" ; "))
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: raid_fixture RESOURCES INPUT OUTPUT")?,
@@ -57,8 +57,8 @@ fn compound(tag: &Tag) -> String {
out
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: recipe_book_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -30,8 +30,8 @@ fn round(v: f32) -> f32 {
(v * 1e4).round() / 1e4
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: recipe_book_render_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -15,8 +15,8 @@ use horizon_client::{
use serde_json::Value;
use std::{fmt::Write as _, fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: recipe_book_view_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -12,8 +12,8 @@ use horizon_client::{
};
use std::{fs, path::Path};
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: recipe_fixture RESOURCES INPUT OUTPUT")?,
@@ -0,0 +1,128 @@
//! The region writer on real chunks, without a window: how long a world's
//! chunks take to be encoded, compressed and written by `SaveDir`, against
//! the same encoding and compression done one chunk after another (what the
//! writer thread did alone before it had helpers).
//!
//! save_bench RESOURCES DIMENSION_DIR [--rounds N]
//!
//! DIMENSION_DIR is a saved dimension folder (`saves/W/dimensions/minecraft/
//! overworld`); its chunks are read once, then each round saves them all into
//! a scratch folder with `save_later` + `flush`, and times the one-thread
//! baseline. The written chunks are checked byte for byte against the
//! baseline's compressed records.
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
save::{SaveDir, SaveTables, encode_with},
};
use horizon_core::position::ChunkPos;
use horizon_world::{
compression::Compression,
nbt::Limits,
region::{ChunkRecord, RegionFile},
};
use std::{path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let usage = "usage: save_bench RESOURCES DIMENSION_DIR [--rounds N]";
let root = PathBuf::from(args.first().ok_or(usage)?);
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
let rounds: usize = args
.iter()
.position(|a| a == "--rounds")
.and_then(|i| args.get(i + 1))
.and_then(|v| v.parse().ok())
.unwrap_or(5);
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Arc::new(Biomes::load(&root)?);
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
let registry = assets.registry;
let source = SaveDir::new(dimension.clone());
let _ = source.tables.set(tables.clone());
let mut chunks = Vec::new();
for entry in std::fs::read_dir(dimension.join("region"))? {
let path = entry?.path();
if path.extension().is_none_or(|e| e != "mca") {
continue;
}
let region = RegionFile::open(&path)?;
for (pos, _) in region.chunks() {
if let Some(chunk) = source.load(pos.x, pos.z, registry, &biomes) {
chunks.push(Arc::new(chunk));
}
}
}
chunks.sort_by_key(|c| (c.x, c.z));
println!("{} chunks from {}", chunks.len(), dimension.display());
let scratch = std::env::temp_dir().join(format!("save_bench_{}", std::process::id()));
// Encoding and compressing on one thread (what the writer did alone),
// then SaveDir's writer with its helpers.
let mut times = [Vec::new(), Vec::new(), Vec::new()];
let mut written = Vec::new();
for round in 0..rounds {
let started = Instant::now();
let tags: Vec<_> = chunks
.iter()
.map(|c| encode_with(c, registry, &biomes, Some(&tables), true))
.collect();
times[0].push(started.elapsed().as_secs_f64());
let started = Instant::now();
let baseline: Vec<ChunkRecord> = tags
.iter()
.map(|tag| ChunkRecord::from_nbt(tag, Compression::Zlib, 0, Limits::default()))
.collect::<std::io::Result<_>>()?;
times[1].push(started.elapsed().as_secs_f64());
drop(tags);
let _ = std::fs::remove_dir_all(&scratch);
let save = SaveDir::new(scratch.clone());
let _ = save.tables.set(tables.clone());
let started = Instant::now();
save.save_later(&chunks, registry, &biomes)?;
save.flush().unwrap();
times[2].push(started.elapsed().as_secs_f64());
drop(save);
if round == 0 {
for (chunk, want) in chunks.iter().zip(&baseline) {
let path = scratch.join(format!("region/r.{}.{}.mca", chunk.x >> 5, chunk.z >> 5));
let got = RegionFile::open(&path)?
.read(ChunkPos::new(chunk.x, chunk.z))?
.ok_or("a saved chunk is missing")?;
if got.compressed != want.compressed {
return Err(format!("chunk {} {} differs", chunk.x, chunk.z).into());
}
}
written = baseline.iter().map(|r| r.compressed.len()).collect();
}
}
let _ = std::fs::remove_dir_all(&scratch);
println!(
"{} KiB compressed, identical to the one-thread records",
written.iter().sum::<usize>() / 1024
);
let per_chunk = 1000.0 / chunks.len() as f64;
let names = [
"one thread: encode",
"one thread: compress",
"SaveDir (encode, compress, write)",
];
for (name, t) in names.iter().zip(&mut times) {
t.sort_by(f64::total_cmp);
println!(
"{name}: best {:.0} ms, median {:.0} ms of {rounds} ({:.3} ms per chunk at best)",
t[0] * 1000.0,
t[t.len() / 2] * 1000.0,
t[0] * per_chunk
);
}
Ok(())
}
@@ -0,0 +1,265 @@
//! Differential save test: load every full chunk of a world written by the
//! vanilla 26.3 server, light it, encode it again with Horizon's chunk writer
//! and compare the result with the original NBT: block states, biomes,
//! heightmaps, light layers (by value), block entities and scheduled ticks.
//!
//! cargo run --profile fast -p horizon-client --example save_parity -- \
//! RESOURCES DIMENSION_DIR [--nether]
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS},
light::{LightTables, apply_lighting, light_chunk},
save::{self, SaveTables},
};
use horizon_core::position::ChunkPos;
use horizon_world::{
chunk::BlockState,
nbt::{Limits, NamedTag, Tag},
region::RegionFile,
};
use std::{
collections::{BTreeMap, HashMap},
path::PathBuf,
};
/// Block state names per section index, decoded independently of Horizon.
fn block_names(tag: &Tag) -> BTreeMap<i64, Vec<String>> {
let mut out = BTreeMap::new();
let Some(Tag::List(sections)) = tag.get("sections") else {
return out;
};
for s in sections {
let Some(Tag::Byte(y)) = s.get("Y") else {
continue;
};
let Some(states) = s.get("block_states") else {
continue;
};
let Some(Tag::List(palette)) = states.get("palette") else {
continue;
};
let names: Vec<String> = palette
.iter()
.map(|p| {
let st = BlockState::from_nbt(p).expect("palette entry");
format!("{}{:?}", st.name, st.properties)
})
.collect();
let bits = (usize::BITS - (names.len().max(1) - 1).leading_zeros()).max(4);
let per = 64 / bits as usize;
let data = match states.get("data") {
Some(Tag::LongArray(d)) => d.clone(),
_ => Vec::new(),
};
let blocks = (0..4096)
.map(|i| {
if names.len() == 1 {
return names[0].clone();
}
let v = (data[i / per] as u64 >> ((i % per) as u32 * bits)) & ((1 << bits) - 1);
names[v as usize].clone()
})
.collect();
out.insert(i64::from(*y), blocks);
}
out
}
fn light(tag: &Tag, key: &str) -> HashMap<i64, Vec<u8>> {
let mut out = HashMap::new();
if let Some(Tag::List(sections)) = tag.get("sections") {
for s in sections {
if let (Some(Tag::Byte(y)), Some(Tag::ByteArray(a))) = (s.get("Y"), s.get(key)) {
out.insert(i64::from(*y), a.clone());
}
}
}
out
}
/// Light value with vanilla's reading of missing layers.
fn value(
layers: &HashMap<i64, Vec<u8>>,
sy: i64,
i: usize,
sky: bool,
empty: bool,
top: i64,
) -> u8 {
let nibble = |a: &Vec<u8>, i: usize| (a[i >> 1] >> ((i & 1) * 4)) & 15;
if let Some(a) = layers.get(&sy) {
return nibble(a, i);
}
if !sky || !empty {
return 0;
}
((sy + 1)..=top)
.find_map(|y| layers.get(&y))
.map_or(15, |a| nibble(a, i & 255))
}
fn count(tag: &Tag, key: &str) -> usize {
match tag.get(key) {
Some(Tag::List(l)) => l.len(),
_ => 0,
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let root = PathBuf::from(
args.first()
.ok_or("usage: save_parity RESOURCES DIMENSION_DIR")?,
);
let dim = PathBuf::from(args.get(1).ok_or("missing dimension directory")?);
let sky_light = !args.iter().any(|a| a == "--nether");
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let lights = LightTables::load(&root, assets.registry, &facts)?;
let tables = SaveTables::new(&assets).with_block_entities(&facts);
let mut originals: HashMap<(i32, i32), NamedTag> = HashMap::new();
let mut chunks: HashMap<(i32, i32), Chunk> = HashMap::new();
for entry in std::fs::read_dir(dim.join("region"))? {
let mut region = RegionFile::open(entry?.path())?;
let positions: Vec<ChunkPos> = region.chunks().map(|(p, _)| p).collect();
for p in positions {
let Some(record) = region.read(p)? else {
continue;
};
let tag = record.nbt(Limits::default())?;
if tag.tag.get("isLightOn") != Some(&Tag::Byte(1)) {
continue;
}
if let Some(c) = save::decode_with(&tag, assets.registry, &biomes, Some(&tables)) {
chunks.insert((c.x, c.z), c);
originals.insert((p.x, p.z), tag);
}
}
}
let mut keys: Vec<_> = chunks
.keys()
.copied()
.filter(|&(x, z)| {
(-1..=1).all(|dz| (-1..=1).all(|dx| chunks.contains_key(&(x + dx, z + dz))))
})
.collect();
keys.sort_unstable();
let (
mut blocks_bad,
mut biomes_bad,
mut heightmaps_bad,
mut light_bad,
mut light_total,
mut other_bad,
) = (0, 0, 0, 0u64, 0u64, 0);
for &(x, z) in &keys {
let region: [[Option<&Chunk>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| chunks.get(&(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let mut chunk = chunks[&(x, z)].clone();
apply_lighting(&mut chunk, light_chunk(&region, &lights, sky_light));
chunk.lit = true;
let ours = save::encode_with(&chunk, assets.registry, &biomes, Some(&tables), sky_light);
let theirs = &originals[&(x, z)];
let (a, b) = (&ours.tag, &theirs.tag);
// Block states: compare every non-empty vanilla section.
let (na, nb) = (block_names(a), block_names(b));
for (y, vb) in &nb {
match na.get(y) {
Some(va) if va == vb => {}
Some(va) => {
let n = va.iter().zip(vb).filter(|(p, q)| p != q).count();
if blocks_bad < 5 {
let i = va.iter().zip(vb).position(|(p, q)| p != q).unwrap_or(0);
eprintln!(
"chunk {x} {z} section {y}: {n} blocks differ, e.g. {} vs {}",
va[i], vb[i]
);
}
blocks_bad += 1;
}
None if vb.iter().all(|s| s.starts_with("minecraft:air")) => {}
None => blocks_bad += 1,
}
}
let biome_of = |t: &Tag| -> Vec<Tag> {
match t.get("sections") {
Some(Tag::List(l)) => l.iter().filter_map(|s| s.get("biomes").cloned()).collect(),
_ => Vec::new(),
}
};
if biome_of(a) != biome_of(b) {
biomes_bad += 1;
}
for kind in [
"WORLD_SURFACE",
"OCEAN_FLOOR",
"MOTION_BLOCKING",
"MOTION_BLOCKING_NO_LEAVES",
] {
let get = |t: &Tag| t.get("Heightmaps").and_then(|h| h.get(kind)).cloned();
if get(a) != get(b) {
if heightmaps_bad < 5 {
eprintln!("chunk {x} {z}: heightmap {kind} differs");
}
heightmaps_bad += 1;
}
}
for (key, sky) in [("SkyLight", true), ("BlockLight", false)] {
if sky && !sky_light {
continue;
}
let (la, lb) = (light(a, key), light(b, key));
let top = i64::from((MIN_Y >> 4) + SECTIONS as i32);
for si in 0..SECTIONS {
let sy = i64::from((MIN_Y >> 4) + si as i32);
let empty = chunk.sections[si].is_uniform(tables_air(&assets));
for i in 0..4096 {
light_total += 1;
if value(&la, sy, i, sky, empty, top) != value(&lb, sy, i, sky, empty, top) {
light_bad += 1;
}
}
}
}
for key in ["block_entities", "block_ticks", "fluid_ticks"] {
if count(a, key) != count(b, key) {
if other_bad < 5 {
eprintln!(
"chunk {x} {z}: {key} {} vs {}",
count(a, key),
count(b, key)
);
}
other_bad += 1;
}
}
for key in [
"structures",
"PostProcessing",
"InhabitedTime",
"Status",
"DataVersion",
"yPos",
] {
if a.get(key) != b.get(key) {
if other_bad < 5 {
eprintln!("chunk {x} {z}: {key} differs");
}
other_bad += 1;
}
}
}
println!(
"{} chunks: {blocks_bad} sections with different blocks, {biomes_bad} chunks with different biomes, \
{heightmaps_bad} different heightmaps, {light_bad} of {light_total} light values differ, {other_bad} other differences",
keys.len()
);
Ok(())
}
fn tables_air(assets: &Assets) -> u32 {
assets
.registry
.default_state("minecraft:air")
.map_or(0, |s| s.0)
}
@@ -28,8 +28,8 @@ fn vector(value: &Value) -> Result<Vec3> {
};
Ok(Vec3::new(number(0)?, number(1)?, number(2)?))
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
if args.len() != 3 {
return Err("usage: scene_collision_probe RESOURCES FIXTURE_DIRECTORY RADIUS".into());
}
@@ -78,7 +78,6 @@ pub fn main() -> Result<()> {
let mut selected = collisions::EntityCollisions {
blocks: scene,
entities: &entities,
border: None,
};
let movement = world_movement::resolve_world_movement(
&mut selected,
@@ -233,8 +233,8 @@ fn snapshot(out: &mut impl Write, player: &PlayerMotion) -> Result<()> {
writeln!(out, "events {}", events.join(" | "))?;
Ok(())
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
if args.len() != 3 {
return Err(
"usage: scene_motion_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into(),
@@ -370,8 +370,8 @@ fn score_json(sc: &sb::Score) -> Value {
})
}
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let registries: Value =
serde_json::from_str(&fs::read_to_string(&args[1]).expect("registries")).expect("json");
let list = |k: &str| -> Vec<String> {
@@ -60,8 +60,8 @@ fn draw_json(d: &ScreenDraw) -> Value {
}
}
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: screen_layout_fixture RESOURCES INPUT OUTPUT";
let banners = Banners::load(Path::new(args.get(1).ok_or(usage)?));
let input = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -136,6 +136,7 @@ fn scene(
blend_radius,
smooth_lighting,
cutout_leaves,
cull_leaves: false,
};
let section_y = int(&row["section_y"])?;
let started = Instant::now();
@@ -169,8 +170,8 @@ fn scene(
}
Ok(json!({"name": row["name"], "quads": quads}))
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
if args.len() != 3 {
return Err(
"usage: section_mesh_fixture RESOURCES (INPUT.jsonl | facts) OUTPUT.jsonl".into(),
@@ -11,8 +11,8 @@ use horizon_client::server::{
use horizon_protocol::crypto::uuid_string;
use std::{fmt::Write as _, path::Path};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = crate::args().skip(1);
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let root = std::path::PathBuf::from(args.next().ok_or("CASES OUTPUT")?);
let output = args.next().ok_or("OUTPUT")?;
let mut cases: Vec<_> = std::fs::read_dir(&root)?
@@ -5,8 +5,8 @@ use horizon_client::music::{BackgroundMusic, Music, MusicTables, Situation};
use horizon_client::sound_events::normalize;
use std::io::Write;
pub fn main() {
let args: Vec<String> = crate::args().collect();
fn main() {
let args: Vec<String> = std::env::args().collect();
let tables = MusicTables::load(std::path::Path::new(&args[1]));
let input = std::fs::read_to_string(&args[2]).expect("input");
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[3]).expect("output"));
Loaded 100 of 1384 files, more files were not shown because too many files have changed in this diff. Show more