Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a058bb2a |
No files matched your search
@@ -1,4 +1,3 @@
|
||||
self-hosted-runner:
|
||||
# The Mac mini's runner (ludovics-mac-mini), which holds the maintainer's own Minecraft files.
|
||||
# windows-11-vs2026-arm is GitHub's Windows ARM64 image (2026-08), newer than actionlint's label list.
|
||||
labels: [horizon-game, ludovics-mac-mini, windows-11-vs2026-arm]
|
||||
labels: [horizon-game, ludovics-mac-mini]
|
||||
@@ -1,16 +1,10 @@
|
||||
# Trusted reviewers: one list for three things (docs/release-flow.md "Trusted reviewers").
|
||||
# Trusted reviewers: one list for two things (docs/release-flow.md "Trusted reviewers").
|
||||
# - `/nightly 2640 2642` picks the pull requests the Nightly holds (tools/ci/nightly_set.py);
|
||||
# - `/close`, `/reopen`, `/label`, `/unlabel` triage issues and pull requests (tools/ci/triage_command.py);
|
||||
# - `/unclaim @login` releases someone else's claim, and listed reviewers have no claim limit (tools/ci/claim_command.py).
|
||||
# One reviewer per line: `<numeric GitHub ID> <login>`, with who added it and when. The ID decides; the login is only
|
||||
# for people reading this file and for messages. A login is a name anyone can claim once its owner renames or deletes
|
||||
# the account; an ID is never reused. Get it with `gh api users/<login> --jq .id`, or run
|
||||
# `python3 tools/ci/resolve_reviewer_ids.py` to fill in the IDs and `--check` to catch renames.
|
||||
# A line in any other form (a bare login included) grants nothing. Ludovic is always allowed (OWNER_ID in
|
||||
# tools/ci/nightly_set.py) and isn't listed. Removing a line revokes that account at once. A line with only `paused`
|
||||
# stops all three for every reviewer (Ludovic's still work).
|
||||
54669067 kacpherek # added by Ludovic, 2026-10-27
|
||||
87152661 rdbtCVS # added by Ludovic, 2026-10-27
|
||||
84875247 Online5277 # added by Ludovic, 2026-10-27 (his top three by merged pull requests)
|
||||
86072227 levzzz5154 # added by Ludovic, 2026-10-28 (Horizon Team; author of horizon-mixins)
|
||||
87478350 Qwerner88880 # added by Ludovic, 2026-10-31 (Discord vladislav_hagen; native-mod hooks)
|
||||
# - `/close`, `/reopen`, `/label`, `/unlabel` triage issues and pull requests (tools/ci/triage_command.py).
|
||||
# Ludovic is always allowed and isn't listed. One GitHub login per line, with who added it and when. Removing a
|
||||
# line revokes that login at once. A line with only `paused` stops both for every reviewer (Ludovic's still work).
|
||||
kacpherek # added by Ludovic, 2026-10-27
|
||||
rdbtCVS # added by Ludovic, 2026-10-27
|
||||
Online5277 # added by Ludovic, 2026-10-27 (his top three by merged pull requests)
|
||||
levzzz5154 # added by Ludovic, 2026-10-28 (Horizon Team; author of horizon-mixins)
|
||||
Qwerner88880 # added by Ludovic, 2026-10-31 (Discord vladislav_hagen; native-mod hooks)
|
||||
@@ -9,9 +9,9 @@ permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
cancel:
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: no checkout, no code of the pull request
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: no checkout, no code of the pull request
|
||||
# (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Cancel the pull request's unfinished runs
|
||||
env:
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
name: cargo-horizon-mod
|
||||
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120): binstall reads
|
||||
# [package.metadata.binstall] from crates/cargo-horizon-mod/Cargo.toml and downloads the archive this workflow
|
||||
# attaches to the tag `cargo-horizon-mod-v<version>`, whose version must be the manifest's. The targets come from
|
||||
# tools/horizon_mod_binstall.py, the same file the manifest's template is checked against, so a platform added there
|
||||
# is built here and one nobody publishes is caught before it ships. A pull request builds the archives and publishes
|
||||
# nothing.
|
||||
on:
|
||||
push:
|
||||
tags: ['cargo-horizon-mod-v*']
|
||||
pull_request:
|
||||
paths:
|
||||
- 'crates/cargo-horizon-mod/**'
|
||||
- 'tools/horizon_mod_binstall.py'
|
||||
- '.github/workflows/cargo-horizon-mod.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
platforms: ${{ steps.platforms.outputs.platforms }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- id: platforms
|
||||
run: echo "platforms=$(python3 -c 'import json, tools.horizon_mod_binstall as m; print(json.dumps(m.PLATFORMS))')" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
needs: plan
|
||||
name: build ${{ matrix.target }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.plan.outputs.platforms) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: The aarch64 Linux linker
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
|
||||
- name: Build
|
||||
run: cargo build --release --locked -p cargo-horizon-mod --target ${{ matrix.target }}
|
||||
- name: It starts
|
||||
if: matrix.run
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
|
||||
[ -f "$binary.exe" ] && binary="$binary.exe"
|
||||
out=$("$binary" 2>&1 || true)
|
||||
case "$out" in
|
||||
*"usage: cargo horizon-mod"*) ;;
|
||||
*) echo "$out"; echo "::error::$binary does not start"; exit 1 ;;
|
||||
esac
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
|
||||
[ -f "$binary.exe" ] && binary="$binary.exe"
|
||||
mkdir -p dist
|
||||
tar -czf "dist/$(basename "$binary" | sed 's/\.exe$//')-${{ matrix.target }}.tar.gz" -C "$(dirname "$binary")" "$(basename "$binary")"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cargo-horizon-mod-${{ matrix.target }}
|
||||
path: dist/*.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: cargo-horizon-mod-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The tag and the manifest name the same release and archives
|
||||
run: python3 tools/horizon_mod_binstall.py dist "$GITHUB_REF_NAME"
|
||||
- name: Checksums
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd dist
|
||||
for file in *.tar.gz; do sha256sum "$file" | cut -d' ' -f1 > "$file.sha256"; done
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes="\`cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod\`"
|
||||
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
|
||||
else
|
||||
gh release create "$GITHUB_REF_NAME" dist/* --latest=false --title "cargo-horizon-mod ${GITHUB_REF_NAME#cargo-horizon-mod-v}" --notes "$notes"
|
||||
fi
|
||||
@@ -27,24 +27,19 @@ jobs:
|
||||
# One job, not one per check: each job takes a runner from the organisation's shared pool, and with every pull request
|
||||
# and every merge to main starting this workflow, three jobs a run left the pool's queue tens of minutes deep.
|
||||
#
|
||||
# Where (docs/ci-runners.md): our checks runners (label horizon-checks: the desktop's, an isolated user, so a fork's
|
||||
# pull request runs there too) take the pull requests whose number ends in 0 or 1, about what their three runners
|
||||
# can do; GitHub's runners take the others and main's pushes (which also save the Rust cache GitHub's pull request
|
||||
# runs read), so both pools work at once. The repository variable SELF_HOSTED_CI moves them: `all` every job to
|
||||
# ours, `light` (the desktop paused) or `off` every job to GitHub's. On ours the build folder persists
|
||||
# (CARGO_TARGET_DIR, outside the checkout), so the apt packages, Python and the Rust cache steps are GitHub's only.
|
||||
# Where: GitHub's runners. With the repository variable SELF_HOSTED_CI=all (for when that pool is full: their 4 CPUs
|
||||
# are about one GitHub runner, docs/ci-runners.md), main's pushes and this repository's own pull requests run on
|
||||
# playhorizon's checks runners (label playhorizon-ci); a fork's pull request never does (and the runners refuse it
|
||||
# themselves). There the build folder persists (CARGO_TARGET_DIR, outside the checkout), so the apt packages,
|
||||
# Python and the Rust cache steps are GitHub's only.
|
||||
verify:
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
|
||||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI == 'all' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('["self-hosted", "linux", "playhorizon-ci"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v7
|
||||
- uses: actions/setup-python@v5
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
python-version: '3.12'
|
||||
@@ -68,16 +63,6 @@ jobs:
|
||||
else
|
||||
python tools/check_public_tree.py --history
|
||||
fi
|
||||
# Our runners as GitHub's: some Python tests compile Rust with a bare `rustc` (GitHub's image has a default
|
||||
# toolchain, our runners' own rustup homes may not), and the GPU tests find no adapter there, so they return
|
||||
# early. Ours have Mesa's software Vulkan for parity; a backend Linux lacks keeps the tests from it, so a pull
|
||||
# request's verify doesn't depend on which pool took it (#8627: a GPU test that flakes on lavapipe).
|
||||
- name: Our runners as GitHub's (a default Rust toolchain, no GPU)
|
||||
if: runner.environment != 'github-hosted'
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
rustup default stable
|
||||
echo "WGPU_BACKEND=dx12" >> "$GITHUB_ENV"
|
||||
- name: Test offline research tooling
|
||||
run: python -m unittest discover -s tests -v
|
||||
- name: Install Rust toolchain and the ALSA and udev headers the client links against
|
||||
@@ -119,14 +104,10 @@ jobs:
|
||||
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
|
||||
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
|
||||
launcher:
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
|
||||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI == 'all' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('["self-hosted", "linux", "playhorizon-ci"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Does this pull request change the launcher or a crate it builds?
|
||||
@@ -158,114 +139,18 @@ jobs:
|
||||
run: |
|
||||
cargo +stable fmt --check
|
||||
cargo +stable clippy --all-targets --locked -- -D warnings
|
||||
cargo +stable test --locked --lib --bins
|
||||
# Compile and test Windows ARM64 on an actual ARM64 host, and inspect the resulting PE headers. By hand only
|
||||
# (`gh workflow run checks --ref <branch>`): GitHub's runner queue is the bottleneck, and main's runs gate the
|
||||
# releases (tools/release_train.py waits for them), so neither pull requests nor main's pushes start this
|
||||
# 90-minute job. The Nightly and launcher releases build and check these binaries on every run.
|
||||
windows-arm64:
|
||||
name: Windows ARM64 game and launcher
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: windows-11-vs2026-arm
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
RUSTFLAGS: -C target-feature=+crt-static
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use every runner core for Cargo
|
||||
shell: pwsh
|
||||
run: |
|
||||
$cores = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
|
||||
"CARGO_BUILD_JOBS=$cores" >> $env:GITHUB_ENV
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-pc-windows-msvc
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: windows-arm64-game
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: launcher
|
||||
shared-key: windows-arm64-launcher
|
||||
- name: Check LLVM assembler used by ring
|
||||
shell: pwsh
|
||||
run: |
|
||||
$llvmBin = Join-Path $env:ProgramFiles 'LLVM\bin'
|
||||
if (-not (Get-Command clang -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $llvmBin 'clang.exe'))) {
|
||||
$env:PATH = "$llvmBin;$env:PATH"
|
||||
"PATH=$env:PATH" >> $env:GITHUB_ENV
|
||||
}
|
||||
$clang = Get-Command clang -ErrorAction Stop
|
||||
Write-Output "Using $($clang.Source)"
|
||||
& $clang.Source --version
|
||||
- name: Build the game for native Windows ARM64
|
||||
run: cargo build --release --locked --target aarch64-pc-windows-msvc -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
|
||||
- name: Check the game's PE machine
|
||||
run: python tools/check_pe_arch.py arm64 target/aarch64-pc-windows-msvc/release/horizon-client.exe target/aarch64-pc-windows-msvc/release/horizon-ctl.exe target/aarch64-pc-windows-msvc/release/horizon-prepare.exe target/aarch64-pc-windows-msvc/release/horizon-server.exe
|
||||
- name: Check game DLL imports
|
||||
run: python tools/check_imports.py target/aarch64-pc-windows-msvc/release/horizon-client.exe target/aarch64-pc-windows-msvc/release/horizon-ctl.exe target/aarch64-pc-windows-msvc/release/horizon-prepare.exe target/aarch64-pc-windows-msvc/release/horizon-server.exe
|
||||
- name: Start every game executable
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$client = 'target/aarch64-pc-windows-msvc/release/horizon-client.exe'
|
||||
$output = & $client --help 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or ($output -join "`n") -notmatch '--resources') { throw 'horizon-client --help failed' }
|
||||
foreach ($binary in @('horizon-ctl.exe', 'horizon-prepare.exe', 'horizon-server.exe')) {
|
||||
$path = "target/aarch64-pc-windows-msvc/release/$binary"
|
||||
$output = & $path --help 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or -not $output) { throw "$binary --help failed" }
|
||||
}
|
||||
foreach ($binary in @('horizon-prepare.exe', 'horizon-server.exe')) {
|
||||
$path = "target/aarch64-pc-windows-msvc/release/$binary"
|
||||
$output = & $path --version 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or -not $output) { throw "$binary --version failed" }
|
||||
Write-Output ($output -join "`n")
|
||||
}
|
||||
- name: Test and build the native Windows ARM64 launcher
|
||||
working-directory: launcher
|
||||
run: |
|
||||
cargo test --release --locked --target aarch64-pc-windows-msvc --lib
|
||||
cargo build --release --locked --target aarch64-pc-windows-msvc
|
||||
- name: Check the launcher's PE machine
|
||||
working-directory: launcher
|
||||
run: python ../tools/check_pe_arch.py arm64 target/aarch64-pc-windows-msvc/release/horizon-launcher.exe
|
||||
- name: Check launcher DLL imports and command-line startup
|
||||
working-directory: launcher
|
||||
# bash, as launcher.yml's check on the same runner: the launcher is a GUI-subsystem program, and a pwsh
|
||||
# `$output = & launcher --version` read no output from it (run 42217014588).
|
||||
shell: bash
|
||||
run: |
|
||||
launcher=target/aarch64-pc-windows-msvc/release/horizon-launcher.exe
|
||||
python ../tools/check_imports.py "$launcher"
|
||||
out=$("$launcher" --version)
|
||||
echo "$out"
|
||||
[[ "$out" == "horizon-launcher "* ]] || { echo "::error::horizon-launcher --version printed '$out'"; exit 1; }
|
||||
- name: Build a native-mod game with the launcher's managed toolchain
|
||||
working-directory: launcher
|
||||
shell: pwsh
|
||||
env:
|
||||
HORIZON_LAUNCHER_HOME: ${{ runner.temp }}\horizon-native-arm64-toolchain
|
||||
HORIZON_NATIVE_TARGET_DIR: ${{ runner.temp }}\horizon-native-arm64-toolchain\target
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
cargo test --release --locked --target aarch64-pc-windows-msvc --lib rust_toolchain::tests::managed_arm64_toolchain_builds_the_game -- --ignored --exact --nocapture
|
||||
$client = Join-Path $env:HORIZON_NATIVE_TARGET_DIR 'fast/horizon-client.exe'
|
||||
python ../tools/check_pe_arch.py arm64 $client
|
||||
python ../tools/check_imports.py $client
|
||||
cargo +stable test --locked --lib
|
||||
# Most pull requests leave the harnesses to parity-main, which runs them on main a batch of merges at a time
|
||||
# (.github/workflows/parity-main.yml; releases wait for it). A pull request still runs them itself when it changes
|
||||
# a harness, an oracle, a probe or the runner, or carries the run-parity label (run-parity.yml, when it is added).
|
||||
parity-scope:
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
# A git diff of this repository's own branch: one of our small runners (horizon-light, docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
# A git diff of this repository's own branch: one of playhorizon's small runners (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
outputs:
|
||||
run: ${{ steps.scope.outputs.run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -277,7 +162,7 @@ jobs:
|
||||
run: |
|
||||
changed=$(git diff --name-only "$(git merge-base "$BASE_SHA" HEAD)" HEAD)
|
||||
harness=$(grep -E '^(tools/test_[a-z0-9_]+_parity\.py|tools/(parity[a-z_]*|lab)\.py|tools/parity_hosts\.json|mods/horizon/src/test/|crates/[^/]+/examples/|crates/horizon-client/src/game/[a-z_]*(fixture|harness)\.rs|\.github/workflows/parity[a-z-]*\.yml)' <<< "$changed" || true)
|
||||
if [ -n "$harness" ] || [[ " $LABELS " == *" run-parity "* ]] || [[ " $LABELS " == *" frame-identity "* ]]; then
|
||||
if [ -n "$harness" ] || [[ " $LABELS " == *" run-parity "* ]]; then
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
echo "The pull request runs its harnesses: ${harness:-label run-parity}"
|
||||
else
|
||||
@@ -292,7 +177,6 @@ jobs:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
|
||||
frame_identity: ${{ contains(github.event.pull_request.labels.*.name, 'frame-identity') }}
|
||||
# Horizon never downloads the game. These jobs need a self-hosted Linux runner
|
||||
# (label horizon-game) holding the maintainer's own copy: the official launcher's
|
||||
# game directory in HORIZON_MINECRAFT_DIR and the pinned server jar in
|
||||
@@ -301,13 +185,13 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: [self-hosted, linux, horizon-game]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install Rust toolchain
|
||||
run: rustup toolchain install stable --profile minimal
|
||||
- uses: actions/setup-java@v6
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
@@ -649,11 +533,11 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: [self-hosted, linux, horizon-game]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: actions/setup-java@v6
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
|
||||
@@ -29,14 +29,14 @@ jobs:
|
||||
# the skipped runs of ordinary comments must not cancel a waiting one.
|
||||
concurrency:
|
||||
group: claim-${{ github.event.issue.number }}
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
@@ -50,22 +50,20 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BODY: ${{ github.event.comment.body }}
|
||||
AUTHOR: ${{ github.event.comment.user.login }}
|
||||
# Who may act is decided by this numeric ID: a login can be claimed by someone else after a rename.
|
||||
AUTHOR_ID: ${{ github.event.comment.user.id }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: python3 tools/ci/claim_command.py command
|
||||
release:
|
||||
if: github.event_name != 'issue_comment' && github.repository == 'horizon-rust/horizon'
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: game build
|
||||
# Builds the game for every platform from `ref`: the Java exporters, then Linux and the Linux server, Windows x64
|
||||
# and ARM64 on GitHub, and macOS on the Mac mini's runner with a 1500-frame offscreen run of a new world (smoke-test.json): Apple
|
||||
# Builds the game for every platform from `ref`: the Java exporters, then Linux and the Linux server and Windows on
|
||||
# GitHub, macOS on the Mac mini's runner with a 1500-frame offscreen run of a new world (smoke-test.json): Apple
|
||||
# Silicon, and Intel cross-built there for x86_64-apple-darwin and checked under Rosetta 2 (#1161). Called by
|
||||
# release.yml (the releases) and nightly-build.yml (the opt-in Nightly), always from main's copy of this file: the
|
||||
# caller's commit, never the one being built. It holds no secrets: signing the checksums is the callers' own job,
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
# Three full preparations (two isolated, one batched) besides the compile.
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
|
||||
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
|
||||
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: original-exporters
|
||||
path: exporters/
|
||||
@@ -83,10 +83,6 @@ jobs:
|
||||
build:
|
||||
needs: exporters
|
||||
name: build ${{ matrix.name }}
|
||||
# Windows ARM64 and musl are new: a failure never holds the other platforms' release (the signing job
|
||||
# needs this job, and a failed entry of it would fail it). The release steps leave the missing
|
||||
# optional archives out with a warning.
|
||||
continue-on-error: ${{ matrix.name == 'windows-arm64' || matrix.name == 'linux-musl' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -98,28 +94,13 @@ jobs:
|
||||
# The server build too: packaging/server/install.sh installs it on Linux.
|
||||
server_dir: horizon-server-linux-x86_64
|
||||
server_asset: horizon-server-linux-x86_64.tar.gz
|
||||
- name: linux-musl
|
||||
os: ubuntu-22.04
|
||||
musl: true
|
||||
dir: horizon-linux-musl-x86_64
|
||||
asset: horizon-linux-musl-x86_64.tar.gz
|
||||
- name: windows
|
||||
os: windows-latest
|
||||
dir: horizon-windows-x64
|
||||
asset: horizon-windows-x64.zip
|
||||
exe: .exe
|
||||
pe_arch: x64
|
||||
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
|
||||
rustflags: -C target-feature=+crt-static
|
||||
- name: windows-arm64
|
||||
os: windows-11-vs2026-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
dir: horizon-windows-arm64
|
||||
asset: horizon-windows-arm64.zip
|
||||
exe: .exe
|
||||
pe_arch: arm64
|
||||
# The ARM64 runner builds and runs native ARM64 executables; keep the C runtime in the archive.
|
||||
rustflags: -C target-feature=+crt-static
|
||||
- name: macos
|
||||
mac: true
|
||||
dir: horizon-macos-arm64
|
||||
@@ -147,7 +128,7 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
@@ -160,29 +141,21 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted' && !matrix.musl
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
# A release restores only what a release saved: an untrusted build has its own key and saves nothing.
|
||||
shared-key: ${{ inputs.trusted && 'release' || 'nightly' }}-${{ matrix.name }}
|
||||
save-if: ${{ inputs.trusted }}
|
||||
- name: ALSA and udev headers
|
||||
if: runner.os == 'Linux' && !matrix.musl
|
||||
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
|
||||
if: ${{ !matrix.musl }}
|
||||
run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
|
||||
- name: Every program the release folder takes was built
|
||||
if: ${{ !matrix.musl }}
|
||||
run: |
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
test -f "$BIN/$binary${{ matrix.exe }}" || { echo "::error::$BIN/$binary${{ matrix.exe }} is missing"; exit 1; }
|
||||
done
|
||||
- name: Verify Windows executable architecture
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
python tools/check_pe_arch.py "${{ matrix.pe_arch }}" \
|
||||
"$BIN/horizon-client${{ matrix.exe }}" "$BIN/horizon-ctl${{ matrix.exe }}" \
|
||||
"$BIN/horizon-prepare${{ matrix.exe }}" "$BIN/horizon-server${{ matrix.exe }}"
|
||||
- name: The signing keychain, unlocked
|
||||
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
||||
env:
|
||||
@@ -200,7 +173,6 @@ jobs:
|
||||
codesign --verify --strict --verbose=2 "$BIN/$binary"
|
||||
done
|
||||
- name: Only system libraries
|
||||
if: ${{ !matrix.musl }}
|
||||
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
|
||||
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
|
||||
run: |
|
||||
@@ -216,14 +188,13 @@ jobs:
|
||||
- name: Baseline x86-64 CPUs only
|
||||
# No POPCNT or LZCNT at all (the script says why only those): a C library's count intrinsic killed the
|
||||
# game before main on a Core 2 (#7264). Not the Mac builds: every Intel Mac that runs macOS 11 has both.
|
||||
if: runner.os != 'macOS' && matrix.pe_arch != 'arm64' && !matrix.musl
|
||||
if: runner.os != 'macOS'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rustup component add llvm-tools
|
||||
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_cpu_baseline.py \
|
||||
$(for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do echo "$BIN/$binary${{ matrix.exe }}"; done)
|
||||
- name: Every program starts
|
||||
if: ${{ !matrix.musl }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bin=$BIN
|
||||
@@ -268,26 +239,18 @@ jobs:
|
||||
# Published with the release; the publish job checks it again before it publishes anything.
|
||||
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
|
||||
# One smoke-test.json per build, the Apple Silicon one's; the Intel build must pass its own run too.
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: runner.environment == 'self-hosted' && !matrix.target
|
||||
with:
|
||||
name: smoke-test.json
|
||||
path: ${{ runner.temp }}/smoke-test.json
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: original-exporters
|
||||
path: exporters
|
||||
- name: Build, check and package for musl in Alpine
|
||||
if: matrix.musl
|
||||
# Run Docker on the Ubuntu host: JavaScript actions themselves require glibc.
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/work" -w /work \
|
||||
-e HORIZON_VERSION -e HORIZON_NIGHTLY \
|
||||
rust:alpine3.22 sh tools/ci/build-musl.sh
|
||||
- name: Package
|
||||
if: ${{ !matrix.musl }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo run --locked -p xtask -- package "$BIN/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
|
||||
@@ -314,7 +277,7 @@ jobs:
|
||||
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
|
||||
done
|
||||
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.asset }}
|
||||
path: dist/${{ matrix.asset }}
|
||||
@@ -336,7 +299,7 @@ jobs:
|
||||
rm -f "${{ matrix.server_asset }}"
|
||||
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
|
||||
ls -l "${{ matrix.server_asset }}"
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: matrix.server_asset
|
||||
with:
|
||||
name: ${{ matrix.server_asset }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: launcher
|
||||
# Builds the launcher on Linux, macOS, Windows x64 and ARM64, and releases it by itself (docs/release-flow.md): after a
|
||||
# Builds the launcher on Linux, macOS and Windows, and releases it by itself (docs/release-flow.md): after a
|
||||
# push to main that changes the launcher or a crate it builds in (the paths below),
|
||||
# every 10 minutes (a release waiting for merges to settle goes out) and by hand, tools/release_train.py decides as
|
||||
# for the game.
|
||||
@@ -15,9 +15,8 @@ name: launcher
|
||||
# under Rosetta 2 (#1161). With the repository variable MACOS_SIGN_IDENTITY set, Horizon.app and the .dmg are signed with
|
||||
# Developer ID, notarized and stapled, from the runner's signing keychain (packaging/macos/README.md);
|
||||
# unset (forks), they are signed ad hoc.
|
||||
# Windows ships each architecture's launcher in its ZIP (what a launcher updates itself from) and as
|
||||
# horizon-launcher-setup.exe or horizon-launcher-setup-arm64.exe, which opens the per-user installer instead of Home
|
||||
# (launcher/src/installer.rs).
|
||||
# Windows ships the launcher's own binary twice: in the ZIP (what a launcher updates itself from) and as
|
||||
# horizon-launcher-setup.exe, which opens the per-user installer instead of Home (launcher/src/installer.rs).
|
||||
# gh workflow run launcher --ref <branch> # dry run: builds everything, publishes nothing
|
||||
# gh workflow run launcher --ref main -f dry_run=false -f force=true
|
||||
|
||||
@@ -74,7 +73,7 @@ jobs:
|
||||
version: ${{ steps.decide.outputs.version }}
|
||||
dry_run: ${{ steps.mode.outputs.dry_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: mode
|
||||
@@ -103,10 +102,6 @@ jobs:
|
||||
needs: decide
|
||||
if: needs.decide.outputs.release == 'true'
|
||||
name: build ${{ matrix.name }}
|
||||
# Windows ARM64 is new: its failure never holds the other platforms' release (the signing job
|
||||
# needs this job, and a failed entry of it would fail it). The release steps leave the missing
|
||||
# ARM64 files out with a warning.
|
||||
continue-on-error: ${{ matrix.name == 'windows-arm64' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -130,20 +125,9 @@ jobs:
|
||||
os: windows-latest
|
||||
asset: horizon-launcher-windows-x64.zip
|
||||
binary: horizon-launcher.exe
|
||||
pe_arch: x64
|
||||
setup_asset: horizon-launcher-setup.exe
|
||||
# The C runtime linked in: without it the launcher asks for VCRUNTIME140.dll,
|
||||
# which only the Visual C++ redistributable installs.
|
||||
rustflags: -C target-feature=+crt-static
|
||||
- name: windows-arm64
|
||||
os: windows-11-vs2026-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
asset: horizon-launcher-windows-arm64.zip
|
||||
binary: horizon-launcher.exe
|
||||
pe_arch: arm64
|
||||
setup_asset: horizon-launcher-setup-arm64.exe
|
||||
# The C runtime linked in, so the standalone ARM64 launcher needs no redistributable.
|
||||
rustflags: -C target-feature=+crt-static
|
||||
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
|
||||
# Notarizing the app and then the .dmg can take a few minutes each.
|
||||
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
|
||||
@@ -166,7 +150,7 @@ jobs:
|
||||
working-directory: launcher
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.decide.outputs.head }}
|
||||
persist-credentials: false
|
||||
@@ -198,9 +182,6 @@ jobs:
|
||||
# Every test target (the library, the binary, tests/), on each system the launcher ships for.
|
||||
- run: cargo test --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
|
||||
- run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
|
||||
- name: Verify Windows executable architecture
|
||||
if: runner.os == 'Windows'
|
||||
run: python ../tools/check_pe_arch.py "${{ matrix.pe_arch }}" "$BIN/${{ matrix.binary }}"
|
||||
- name: The version is the release's
|
||||
run: |
|
||||
out=$("$BIN/${{ matrix.binary }}" --version)
|
||||
@@ -231,8 +212,7 @@ jobs:
|
||||
"$launcher" links --json
|
||||
# The Intel Mac launcher can come out before any game build has its asset (#1161): then there is nothing
|
||||
# to install yet. Only that entry skips, and only on a version list that parsed and had no build here
|
||||
# (exit 3); a launcher or a script that fails still fails the step. The Windows ARM64 launcher always has
|
||||
# one: it installs the x64 build where a release has no ARM64 one (backend.rs, GAME_FALLBACK).
|
||||
# (exit 3); a launcher or a script that fails still fails the step.
|
||||
if [[ "${{ matrix.name }}" == macos-x64 ]]; then
|
||||
versions=$(retry "$launcher" --json versions)
|
||||
no_build='import json, sys; sys.exit(3 if not any(r["available_here"] for r in json.load(sys.stdin)) else 0)'
|
||||
@@ -256,11 +236,6 @@ jobs:
|
||||
# release linked to a Nix store is a warning here, not a broken launcher.
|
||||
soft() { if [[ "$RUNNER_OS" == "Linux" ]]; then "$@" || echo "::warning::$* failed"; else "$@"; fi; }
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
if ! python ../tools/check_pe_arch.py "${{ matrix.pe_arch }}" "$client"; then
|
||||
# A Nightly without an ARM64 build: the ARM64 launcher installs the x64 one (backend.rs, GAME_FALLBACK).
|
||||
[[ "${{ matrix.pe_arch }}" == arm64 ]] && python ../tools/check_pe_arch.py x64 "$client" || exit 1
|
||||
echo "::warning::the latest game has no ARM64 build: the launcher installed the x64 one"
|
||||
fi
|
||||
python ../tools/check_imports.py "$client"
|
||||
elif [[ "$RUNNER_OS" == "Linux" ]]; then
|
||||
soft python3 ../tools/check_imports.py "$client"
|
||||
@@ -320,8 +295,8 @@ jobs:
|
||||
# installer instead of Home (launcher/src/installer.rs). Its name is what makes it the setup,
|
||||
# so the ZIP's copy stays the launcher. Both go into the release's signed SHA256SUMS.
|
||||
if: runner.os == 'Windows'
|
||||
run: cp "$BIN/${{ matrix.binary }}" "out/${{ matrix.setup_asset }}"
|
||||
- uses: actions/upload-artifact@v7
|
||||
run: cp "$BIN/${{ matrix.binary }}" out/horizon-launcher-setup.exe
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.asset }}
|
||||
path: launcher/out/*
|
||||
@@ -339,12 +314,12 @@ jobs:
|
||||
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
|
||||
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci/sign_release.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
pattern: horizon-launcher-*
|
||||
@@ -356,13 +331,13 @@ jobs:
|
||||
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
TAG: ${{ needs.decide.outputs.tag }}
|
||||
run: bash tools/ci/sign_release.sh "$TAG" dist/horizon-launcher-*
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signature
|
||||
path: dist/SHA256SUMS*
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: needs.decide.outputs.dry_run == 'true'
|
||||
with:
|
||||
name: ephemeral.pub
|
||||
@@ -376,7 +351,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Notes
|
||||
@@ -386,7 +361,7 @@ jobs:
|
||||
python3 tools/release_train.py notes --kind launcher --tag "${{ needs.decide.outputs.tag }}" \
|
||||
--head "${{ needs.decide.outputs.head }}" --out notes > /dev/null
|
||||
{ echo "## $(cat notes/title.txt)"; echo; cat notes/body.md; } >> "$GITHUB_STEP_SUMMARY"
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
@@ -398,13 +373,6 @@ jobs:
|
||||
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
|
||||
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
|
||||
# Never marked latest: Horizon's own releases stay the repository's latest.
|
||||
- name: The Windows ARM64 launcher, if it built
|
||||
# Its build may fail without holding the release (continue-on-error): the release then has no ARM64
|
||||
# files, and ARM64 launchers stay on their version until one has (backend::launcher_update_from).
|
||||
run: |
|
||||
for file in horizon-launcher-windows-arm64.zip horizon-launcher-setup-arm64.exe; do
|
||||
[[ -f "dist/$file" ]] || echo "::warning::$file is missing: this release goes out without it"
|
||||
done
|
||||
- name: Publish ${{ needs.decide.outputs.tag }}
|
||||
if: needs.decide.outputs.dry_run != 'true'
|
||||
env:
|
||||
@@ -419,7 +387,7 @@ jobs:
|
||||
run: |
|
||||
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }}: $(cat notes/title.txt)"
|
||||
ls -l dist
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: launcher-release-notes
|
||||
path: notes/
|
||||
|
||||
@@ -15,23 +15,17 @@ on:
|
||||
- '.github/workflows/mod-registry.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: mod-registry-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
check:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: crates/horizon-mod-registry
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Ours keep their build folder between jobs.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: crates/horizon-mod-registry
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -21,11 +21,10 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
# One of our small runners (label horizon-light, docs/ci-runners.md): main's script, Mojang's public manifest.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check the manifest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
pull-requests: write
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -52,8 +52,4 @@ jobs:
|
||||
git fetch origin "+refs/heads/$branch:refs/remotes/origin/$branch"
|
||||
fi
|
||||
git push --force-with-lease origin "$branch"
|
||||
# The repo doesn't let GitHub Actions open pull requests (that setting would also let it approve them):
|
||||
# the branch is pushed, and a maintainer opens the PR from it.
|
||||
if ! gh pr create --base main --head "$branch" --title 'Stamp native hook API fragments' --body 'Assign API numbers to the pending native hook fragments. Close and reopen this PR to run CI.'; then
|
||||
echo "::warning::Stamped fragments are on $branch; open a pull request from it (GitHub Actions may not open one here)."
|
||||
fi
|
||||
gh pr create --base main --head "$branch" --title 'Stamp native hook API fragments' --body 'Assign API numbers to the pending native hook fragments. Close and reopen this PR to run CI.'
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
source_tag: ${{ steps.pick.outputs.source_tag }}
|
||||
dry_run: ${{ steps.mode.outputs.dry_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -124,14 +124,14 @@ jobs:
|
||||
env:
|
||||
SHA: ${{ steps.pick.outputs.sha }}
|
||||
run: git archive --format=tar.gz --prefix="horizon-${SHA:0:7}/" -o horizon-source.tar.gz "$SHA"
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.pick.outputs.build == 'true'
|
||||
with:
|
||||
name: horizon-source
|
||||
path: horizon-source.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-notes
|
||||
path: nightly-notes.md
|
||||
@@ -155,42 +155,32 @@ jobs:
|
||||
# The key's only job: main's sign_release.sh hashes the archives and signs with the runner's OpenSSL.
|
||||
environment: ${{ needs.pick.outputs.dry_run != 'true' && 'release' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci/sign_release.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
pattern: horizon-*
|
||||
merge-multiple: true
|
||||
- name: Sign the checksums for the nightly channel (SHA256SUMS.sig) and for this build (SHA256SUMS.v2.sig)
|
||||
# SHA256SUMS.sig is for launchers from before v2 (signed for "nightly"); SHA256SUMS.v2.sig is signed for
|
||||
# nightly-<date>-<commit>, the version the release title names, which current launchers and servers
|
||||
# require (docs/security.md, "Nightly signatures"). The publish step titles the release from the same values.
|
||||
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig) for the nightly channel
|
||||
env:
|
||||
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
|
||||
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
|
||||
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
ALSO_FOR: nightly-${{ needs.pick.outputs.date }}-${{ needs.pick.outputs.short }}
|
||||
run: |
|
||||
# Windows ARM64's build may fail without holding the Nightly (game-build.yml): then it is left out.
|
||||
arm64=(); if [[ -f dist/horizon-windows-arm64.zip ]]; then arm64=(dist/horizon-windows-arm64.zip)
|
||||
else echo "::warning::horizon-windows-arm64.zip is missing: SHA256SUMS goes out without it"; fi
|
||||
musl=(); if [[ -f dist/horizon-linux-musl-x86_64.tar.gz ]]; then musl=(dist/horizon-linux-musl-x86_64.tar.gz)
|
||||
else echo "::warning::horizon-linux-musl-x86_64.tar.gz is missing: SHA256SUMS goes out without it"; fi
|
||||
bash tools/ci/sign_release.sh nightly dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
|
||||
"${arm64[@]}" "${musl[@]}" \
|
||||
dist/horizon-macos-arm64.zip dist/horizon-macos-x64.zip dist/horizon-server-linux-x86_64.tar.gz \
|
||||
dist/horizon-source.tar.gz
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signature
|
||||
path: dist/SHA256SUMS*
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: needs.pick.outputs.dry_run == 'true'
|
||||
with:
|
||||
name: ephemeral.pub
|
||||
@@ -205,24 +195,19 @@ jobs:
|
||||
contents: write
|
||||
issues: read # the open release-blocker issues (gate)
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The signatures, checked as launchers check them
|
||||
- name: The signature, checked as launchers check it
|
||||
env:
|
||||
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
DATE: ${{ needs.pick.outputs.date }}
|
||||
SHORT: ${{ needs.pick.outputs.short }}
|
||||
run: |
|
||||
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
|
||||
cargo run --locked --quiet -p horizon-release-sign -- verify --tag nightly --publishing "${key[@]}" dist
|
||||
# The version's own signature, which current launchers require: the title below names the same version.
|
||||
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "nightly-${DATE}-${SHORT}" \
|
||||
--signature SHA256SUMS.v2.sig --publishing "${key[@]}" dist
|
||||
- name: Ready to publish (the smoke test drew a world, no release-blocker is open)
|
||||
# Right before publishing, as for a release: an issue labelled while the builds ran still holds it. A dry run
|
||||
# only reports what a real run would do (that is how a blocker's fix is tried).
|
||||
@@ -239,37 +224,18 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SHA: ${{ needs.pick.outputs.sha }}
|
||||
SHORT: ${{ needs.pick.outputs.short }}
|
||||
DATE: ${{ needs.pick.outputs.date }}
|
||||
SOURCE_TAG: ${{ needs.pick.outputs.source_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 tools/ci/nightly.py rotate
|
||||
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
|
||||
[[ -f dist/SHA256SUMS.v2.sig ]] && signature+=(dist/SHA256SUMS.v2.sig)
|
||||
# The title is what launchers parse (strictly) into the version SHA256SUMS.v2.sig is signed for.
|
||||
[[ "${SHA:0:7}" == "$SHORT" ]] || { echo "::error::the build's commit isn't the one signed"; exit 1; }
|
||||
# The ARM64 archive only when its build succeeded (the sign job left it out of SHA256SUMS otherwise).
|
||||
arm64=()
|
||||
if [[ -f dist/horizon-windows-arm64.zip ]]; then
|
||||
arm64=("dist/horizon-windows-arm64.zip#Game files the launcher installs (Nightly): Windows ARM64 (horizon-windows-arm64.zip)")
|
||||
else
|
||||
echo "::warning::horizon-windows-arm64.zip is missing: the Nightly goes out without it (ARM64 launchers install the x64 build)"
|
||||
fi
|
||||
musl=()
|
||||
if [[ -f dist/horizon-linux-musl-x86_64.tar.gz ]]; then
|
||||
musl=("dist/horizon-linux-musl-x86_64.tar.gz#Game files for musl Linux (horizon-linux-musl-x86_64.tar.gz)")
|
||||
else
|
||||
echo "::warning::horizon-linux-musl-x86_64.tar.gz is missing: the Nightly goes out without it"
|
||||
fi
|
||||
# A pre-release, never GitHub's latest: the latest stays the last vX.Y.Z, which launchers and servers from
|
||||
# before the Nightly-only switch keep (they don't read Nightlies).
|
||||
gh release create nightly --prerelease --latest=false --target "$SHA" \
|
||||
--title "Nightly $DATE · ${SHA:0:7}" --notes-file dist/nightly-notes.md \
|
||||
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs (Nightly): Linux (horizon-linux-x86_64.tar.gz)" \
|
||||
"${musl[@]}" \
|
||||
"dist/horizon-windows-x64.zip#Game files the launcher installs (Nightly): Windows (horizon-windows-x64.zip)" \
|
||||
"${arm64[@]}" \
|
||||
"dist/horizon-macos-arm64.zip#Game files the launcher installs (Nightly): macOS on Apple Silicon (horizon-macos-arm64.zip)" \
|
||||
"dist/horizon-macos-x64.zip#Game files the launcher installs (Nightly): macOS on Intel (horizon-macos-x64.zip)" \
|
||||
"dist/horizon-server-linux-x86_64.tar.gz#Nightly dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
|
||||
@@ -287,8 +253,7 @@ jobs:
|
||||
# Tells the thread that asked for this build how it ended. A conflict `pick` already explained there.
|
||||
needs: [pick, build, sign, publish]
|
||||
if: always() && inputs.issue != '' && needs.pick.result == 'success'
|
||||
# One comment, no checkout and no secret: one of our small runners (label horizon-light, docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
issues: write # answers in the thread
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
@@ -41,8 +41,6 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BODY: ${{ github.event.comment.body }}
|
||||
AUTHOR: ${{ github.event.comment.user.login }}
|
||||
# Who may act is decided by this numeric ID: a login can be claimed by someone else after a rename.
|
||||
AUTHOR_ID: ${{ github.event.comment.user.id }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
COMMENT_URL: ${{ github.event.comment.html_url }}
|
||||
run: python3 tools/ci/nightly_set.py command
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
else
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.when.outputs.run == 'true'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
run: python3 tools/ci/parity_issues.py --since "${{ steps.audit.outputs.since }}" ${{ inputs.dry_run && '--dry-run' || '' }} | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
- name: Logs of the failed harnesses
|
||||
if: steps.when.outputs.run == 'true' && steps.audit.outputs.exit != '0'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-parity-logs
|
||||
path: .local/parity-runner/logs/
|
||||
|
||||
@@ -19,15 +19,15 @@ jobs:
|
||||
concurrency:
|
||||
group: overlap-note-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: it runs main's script only (above), so a fork's
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: it runs main's script only (above), so a fork's
|
||||
# pull request runs none of its code there (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -22,24 +22,17 @@ env:
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md): the branch and the base on the same machine, as
|
||||
# the comparison needs. SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust and the ALSA and udev headers
|
||||
env:
|
||||
HOSTED: ${{ runner.environment == 'github-hosted' }}
|
||||
run: |
|
||||
if [ "$HOSTED" = true ]; then
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
fi
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
rustup toolchain install stable --profile minimal
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
shared-key: pack-bench
|
||||
- name: Bench the branch
|
||||
@@ -53,15 +46,11 @@ jobs:
|
||||
SCALE: ${{ inputs.scale || '1' }}
|
||||
run: |
|
||||
git fetch https://github.com/horizon-rust/horizon.git "$BASE"
|
||||
# Our runners keep the folder beside the checkout between jobs.
|
||||
git worktree remove --force ../base 2> /dev/null || rm -rf ../base
|
||||
git worktree prune
|
||||
git worktree add ../base FETCH_HEAD
|
||||
cp crates/horizon-client-render/examples/pack_load_bench.rs ../base/crates/horizon-client-render/examples/
|
||||
# A bench row that times a function the base lacks keeps it from building there: then
|
||||
# the branch's own rows compare the two ways.
|
||||
target=${CARGO_TARGET_DIR:-$GITHUB_WORKSPACE/target}
|
||||
(cd ../base && CARGO_TARGET_DIR="$target" cargo run --profile fast -p horizon-client-render \
|
||||
(cd ../base && CARGO_TARGET_DIR="$GITHUB_WORKSPACE/target" cargo run --profile fast -p horizon-client-render \
|
||||
--example pack_load_bench -- --scale "$SCALE") > base.md \
|
||||
|| echo "The bench does not build on the base." > base.md
|
||||
- name: Summary
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: [self-hosted, macOS, horizon-parity-main]
|
||||
timeout-minutes: 240 # a core-crate batch reruns ~300 harnesses (~60-90 min), a bisect a few rounds of the failures
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
python3 tools/parity_main.py bisect "$BASE_SHA" --comment
|
||||
- name: Logs of the failed harnesses
|
||||
if: steps.harnesses.outcome == 'failure'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: parity-main-logs
|
||||
path: .local/parity-main-logs/
|
||||
|
||||
@@ -20,12 +20,6 @@ on:
|
||||
busy the job waits for it.
|
||||
type: boolean
|
||||
default: false
|
||||
frame_identity:
|
||||
description: >-
|
||||
The pull request carries the frame-identity label: it must leave the picture alone (HDR, shader packs), so
|
||||
the frame_identity harness, which no automatic selection picks, runs too.
|
||||
type: boolean
|
||||
default: false
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
@@ -39,7 +33,7 @@ jobs:
|
||||
# harnesses at a time) up to a few hours, and any pull request's job may land there.
|
||||
timeout-minutes: ${{ inputs.full && 360 || 180 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The commit the event names: a fork's later pushes need another run-parity label. The next step merges
|
||||
# main into it.
|
||||
@@ -93,15 +87,9 @@ jobs:
|
||||
PARITY_HOSTS: ''
|
||||
BASE_SHA: ${{ steps.merge.outputs.base }}
|
||||
run: python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400 # world_spawn takes ~1480 s on a busy runner
|
||||
- name: The pictures this change must leave alone
|
||||
if: ${{ !cancelled() && inputs.frame_identity }}
|
||||
env:
|
||||
PARITY_HOSTS: ''
|
||||
BASE_SHA: ${{ steps.merge.outputs.base }}
|
||||
run: python3 tools/parity.py frame_identity --timeout 2400
|
||||
- name: Logs of the failed harnesses
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: parity-logs
|
||||
path: .local/parity-runner/logs/
|
||||
|
||||
@@ -16,15 +16,15 @@ jobs:
|
||||
label:
|
||||
# Title-only edits change nothing: skip them before a runner starts.
|
||||
if: github.event.action != 'edited' || github.event.changes.body != null
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: it runs main's script only (above), so a fork's
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: it runs main's script only (above), so a fork's
|
||||
# pull request runs none of its code there (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
urgent: ${{ steps.decide.outputs.urgent }}
|
||||
dry_run: ${{ steps.mode.outputs.dry_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: mode
|
||||
|
||||
@@ -27,4 +27,3 @@ jobs:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
|
||||
frame_identity: ${{ contains(github.event.pull_request.labels.*.name, 'frame-identity') }}
|
||||
@@ -15,23 +15,17 @@ on:
|
||||
- '.github/workflows/telemetry-server.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: telemetry-server-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
check:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: crates/horizon-telemetry-server
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Ours keep their build folder between jobs.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: crates/horizon-telemetry-server
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -21,15 +21,14 @@ jobs:
|
||||
startsWith(github.event.comment.body, '/reopen') ||
|
||||
startsWith(github.event.comment.body, '/label') ||
|
||||
startsWith(github.event.comment.body, '/unlabel'))
|
||||
# One of our small runners (label horizon-light, docs/ci-runners.md): main's script only, as above.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
@@ -43,8 +42,6 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BODY: ${{ github.event.comment.body }}
|
||||
AUTHOR: ${{ github.event.comment.user.login }}
|
||||
# Who may act is decided by this numeric ID: a login can be claimed by someone else after a rename.
|
||||
AUTHOR_ID: ${{ github.event.comment.user.id }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
COMMENT_URL: ${{ github.event.comment.html_url }}
|
||||
|
||||
@@ -9,10 +9,8 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
|
||||
- **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` (logs, crash reports, telemetry consent, recordings; not saves), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
|
||||
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`. Saves, `options.txt` and
|
||||
caches go in the game folder, by default the working directory: start the game from a scratch folder with
|
||||
`--game-dir` pointing at it, or they land in the repo root, hidden by `.gitignore`. For the
|
||||
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`, and, for the
|
||||
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. 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 the person whose agent opened it and
|
||||
@@ -34,10 +32,7 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
|
||||
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
|
||||
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
|
||||
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
|
||||
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
|
||||
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
|
||||
`gh issue list --search 'is:open no:assignee -label:"in progress" -label:tracker -label:proposal -label:"not claimable" -linked:pr'`
|
||||
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
|
||||
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
|
||||
|
||||
## Crates
|
||||
|
||||
@@ -51,7 +46,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
|
||||
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
|
||||
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
|
||||
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
|
||||
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings), modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
|
||||
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
|
||||
| `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` |
|
||||
@@ -65,7 +60,6 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
|
||||
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
|
||||
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
|
||||
| `horizon-optifine` | OptiFine-format pack files below every feature that reads them: `.properties`, the name/range/block matchers, paths and `optifine/` folders, and custom entity models (`cem/`: `.jem`/`.jpm` parsing and baking) |
|
||||
| `horizon-polytone` | Polytone-format pack files (`assets/*/polytone/`), read clean-room from the public wiki: targets, biome and dimension modifiers and their environment attribute layers (#11486) |
|
||||
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
|
||||
| `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 |
|
||||
@@ -93,8 +87,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
|
||||
```sh
|
||||
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
|
||||
python3 tools/pr_ready.py --quick # same without harnesses
|
||||
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the examples (pr_ready does this); `--doc` separately
|
||||
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
|
||||
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
|
||||
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
|
||||
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
|
||||
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
|
||||
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
|
||||
@@ -106,8 +100,8 @@ 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` + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
|
||||
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (mostly horizon-world and horizon-client),
|
||||
- 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.
|
||||
- Developing a native mod? Run `horizon-launcher settings native-profile dev` once. `dev` builds the game crate
|
||||
@@ -121,7 +115,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
|
||||
Source paths remain in `crates/horizon-client/`. Measure actual harness selection on refreshed
|
||||
records with `tools/parity.py --changed-files paths.txt --list`; historical counts are not a guarantee.
|
||||
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
|
||||
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio, controllers, `discord`, `friends` and `heartbeat` (the server build).
|
||||
- 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`
|
||||
@@ -152,23 +146,23 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
|
||||
| Subsystem | Files |
|
||||
|---|---|
|
||||
| Game loop, tick, frame | `game.rs`: `fn run`, `fn tick`, `fn tick_world`, `fn render`; G: `window_handler.rs` (`impl ApplicationHandler for Game`: `window_event`, `about_to_wait`); L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.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`, `screen_backgrounds.rs` (panorama, menu blur, border wall) |
|
||||
| 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/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `world_entry.rs`, `postprocess.rs`; game.rs `fn update_world` |
|
||||
| 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`, `spans.rs`; `horizon-client-input/src/window.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/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `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 tick_mining`, `fn break_block`); G: `item_actions.rs` (`fn use_item`), `melee.rs` (`fn attack`), `hand.rs`, `combat.rs`, `consume.rs`, `using_item.rs` (an item in use, tick by tick), `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.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`, `inventory_input.rs` (keys and clicks over a container), `inventory_slots.rs` (slot layout and drawing) |
|
||||
| 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`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.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: crate `horizon-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`; `horizon-client-core`: `nbt_path.rs`, `scoreboard.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`, `loading_screen.rs` (the loading screen, the pre-game `App`), `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs`, `creative_screen.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`, `horizon-anticheat` (L: `lan/anticheat.rs`) |
|
||||
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities), `world_session.rs` (opening and closing a world); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
|
||||
| Input | game.rs `fn window_event`; G: `play_keys.rs` (`fn key_pressed`), `play_input.rs` (`fn play_mouse`, toggled keys, the hotbar wheel); crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs`, `pad_craft.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
|
||||
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.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`; crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.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`, `mobs/cem.rs` + `mobs/cem_anim.rs` (OptiFine/EMF entity models and their animations); `horizon-client-core/src/lang.rs`; crates `horizon-assets`, `horizon-optifine`; 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)) |
|
||||
@@ -177,7 +171,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
|
||||
## How `Game` is organised
|
||||
|
||||
- `struct Game` (game.rs) holds everything. Its methods are spread over many files: game.rs keeps
|
||||
- `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 stay private to the gameplay module and its descendants. Gameplay tests run in the gameplay library, separate from reusable-library tests.
|
||||
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
|
||||
@@ -191,7 +185,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
`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, their methods in `game/resource_tables.rs`.
|
||||
(`pack`), both in game.rs.
|
||||
|
||||
## Single player runs on a server thread
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ everything is that **it plays exactly like vanilla**. This page is the short ver
|
||||
welcome on [Discord](https://discord.gg/playhorizon).
|
||||
|
||||
This repository holds the game and the launcher, plus the tools, tests and docs that build, check and release them.
|
||||
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything
|
||||
else (videos, Discord and other community material) lives elsewhere.
|
||||
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything else (videos, Discord and other community material) lives elsewhere.
|
||||
|
||||
**Where to start: [playhorizon.dev/hub](https://playhorizon.dev/hub).** It shows what is being done now and next
|
||||
([ROADMAP.md](ROADMAP.md) in the repository), the work board, good first tasks, and the forms to report a bug or
|
||||
@@ -15,25 +14,23 @@ queue (the hub, or `/suggest` on Discord) rather than to issues, which are for b
|
||||
|
||||
## Build it
|
||||
|
||||
- **Prerequisites:** Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers
|
||||
(`libasound2-dev libudev-dev`).
|
||||
- **Resources:** Horizon never downloads the game; it prepares its resources from your own copy of Minecraft 26.3, as
|
||||
described in [Build from source](README.md#build-from-source).
|
||||
- **Commands:**
|
||||
You need Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers (`libasound2-dev libudev-dev`).
|
||||
Horizon never downloads the game: it prepares its resources from your own copy of Minecraft 26.3, as described in
|
||||
[Build from source](README.md#build-from-source).
|
||||
|
||||
```sh
|
||||
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
|
||||
cargo run --release -p horizon-client
|
||||
```
|
||||
```sh
|
||||
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
|
||||
cargo run --release -p horizon-client
|
||||
```
|
||||
|
||||
- **Launcher:** its own Cargo workspace in `launcher/` (`cd launcher && cargo run`; on NixOS, `nix-shell` first).
|
||||
The launcher is its own Cargo workspace in `launcher/` (`cd launcher && cargo run`; on NixOS, `nix-shell` first).
|
||||
|
||||
## Vanilla first
|
||||
|
||||
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here means
|
||||
Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a **harness**: a
|
||||
`tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust probe against Horizon,
|
||||
and compares their outputs.
|
||||
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here
|
||||
means Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a
|
||||
**harness**: a `tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust
|
||||
probe against Horizon, and compares their outputs. There are about 290 of them.
|
||||
|
||||
- **Read the vanilla code before writing Horizon's.** `tools/rea_vanilla.py` queries the pinned game bytecode:
|
||||
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`.
|
||||
@@ -44,10 +41,11 @@ and compares their outputs.
|
||||
off by default wherever they would change the vanilla look.
|
||||
- **Everything players run is Rust.** Python and Java are fine for dev tools and harness drivers in `tools/`, never
|
||||
for player-facing code.
|
||||
- **No game files in the repo.** 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).
|
||||
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
|
||||
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
|
||||
|
||||
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).
|
||||
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
|
||||
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
|
||||
|
||||
## Only original code, or code whose license allows it
|
||||
|
||||
@@ -56,27 +54,24 @@ another project's source may also be read and used, **only when that project's l
|
||||
|
||||
- **Allowed:** MIT, MIT-0, BSD-2-Clause, BSD-3-Clause, 0BSD, ISC, zlib, Apache-2.0, Unlicense, and the CC0-1.0
|
||||
public-domain dedication. That code may be read, copied or adapted.
|
||||
- **Off limits, even to read:** GPL, LGPL (Iris), AGPL, MPL, EPL, Polyform (Sodium), the Creative Commons licenses
|
||||
(CC BY, CC BY-SA, CC BY-NC and the others; CC0 is a public-domain dedication, not one of them), custom or
|
||||
"source available" licenses, all rights reserved (OptiFine), and any project with no license or an unclear one.
|
||||
Don't read, copy, translate or paraphrase their source, and don't send code an AI tool wrote from it. Being able
|
||||
to download a mod or a pack does not give you the right to read its files.
|
||||
- **Java's own library is GPL too.** OpenJDK's source (`java.lang.Math`, `StrictMath`, `FdLibm`, `HashMap`, the
|
||||
sorts, `Double.toString`, HotSpot's intrinsics...) is GPL-2.0, so it is off limits like the rest. Reproduce Java's
|
||||
behaviour from its API documentation (the Javadoc) and from what a JVM returns, or from a permissive original such
|
||||
as Sun's fdlibm C sources (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); see #6741.
|
||||
- **Everything else is off limits, even to read:** GPL, LGPL (Iris), AGPL, MPL, EPL, Polyform (Sodium), the Creative
|
||||
Commons licenses (CC BY, CC BY-SA, CC BY-NC and the others; CC0 is a public-domain dedication, not one of them),
|
||||
custom or "source available" licenses, all rights reserved (OptiFine), and any project with no license or an
|
||||
unclear one. Don't read, copy, translate or paraphrase their source, and don't send code an AI tool
|
||||
wrote from it. Being able to download a mod or a pack does not give you the right to read its files.
|
||||
- **Check the license yourself.** Read the LICENSE file at the commit you use and the header of each file you take:
|
||||
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one at
|
||||
your commit counts. In doubt, don't: ask in #contributors first.
|
||||
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one
|
||||
at your commit counts. In doubt, don't: ask in #contributors first.
|
||||
- **Say it in the pull request:** the project, a link to the commit, its license, and what you read or copied. A
|
||||
pull request that used another project's source without saying so is closed.
|
||||
- **Copied or adapted code** keeps its copyright line and license notice at the top of the file, and gets an entry
|
||||
in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Apache-2.0 code also keeps its NOTICE text and says what
|
||||
was changed.
|
||||
|
||||
User documentation and file formats are fine to read, and packs and mods are fine to run and observe, whatever their
|
||||
license. Shader packs are a clean-room rewrite of the pack format, produced from a written description of it (see
|
||||
#1561); they carry no foreign source, and the rules above still apply to anything you read while working on them.
|
||||
User documentation and file formats are fine to read, and packs and mods are fine to run and observe, whatever
|
||||
their license. 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
|
||||
|
||||
@@ -92,8 +87,8 @@ your change can affect. Without the vanilla data on your machine it lists those
|
||||
`--quick` skips the harnesses. It exits non-zero when something fails.
|
||||
|
||||
For the game workspace, Clippy checks all targets of the touched crates and their transitive workspace dependents,
|
||||
including examples. The dependency scope includes optional, platform, build and development dependencies. Tests run
|
||||
library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
|
||||
including examples. The dependency scope includes optional, platform, build and development dependencies.
|
||||
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
|
||||
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
|
||||
|
||||
## Claim before you start
|
||||
@@ -128,8 +123,8 @@ it (edited on every push, never repeated): talk to its author in the thread befo
|
||||
that label, and area labels (`launcher`, `mobs`, `worldgen`, `rendering`...) from the paths you change.
|
||||
`label:parity` is the list of real progress toward vanilla.
|
||||
- **Release labels** (we set them if you can't): `feature` also puts it under "New" in the release notes,
|
||||
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix. `release-now` is for crash
|
||||
fixes that shouldn't wait. pr_ready suggests one.
|
||||
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
|
||||
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
|
||||
|
||||
Who reviews and merges: the Horizon Team, on CI for ordinary pull requests; risky ones (saves, network, world
|
||||
generation, the launcher's updates) wait for a local proof first ([merging](docs/release-flow.md#merging)).
|
||||
|
||||
@@ -37,10 +37,8 @@ members = [
|
||||
"crates/horizon-port-pack",
|
||||
"crates/horizon-shaders",
|
||||
"crates/horizon-optifine",
|
||||
"crates/horizon-polytone",
|
||||
"crates/horizon-shader-api",
|
||||
"crates/horizon-release-sign",
|
||||
"crates/horizon-mod-deps",
|
||||
"xtask",
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -48,7 +46,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
rust-version = "1.95"
|
||||
rust-version = "1.85"
|
||||
|
||||
# Ports mirror vanilla's Java signatures, index loops and nested generic types
|
||||
# one to one so they can be checked against the decompiled sources.
|
||||
@@ -82,21 +80,6 @@ libmimalloc-sys = { path = "third_party/libmimalloc-sys" }
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
|
||||
# The native mod API is generated by a build script that parses every file of game/ with syn on
|
||||
# each gameplay edit; unoptimized that took ~6 s of every rebuild, optimized ~1.5 s.
|
||||
[profile.dev.package.horizon-mod-api-gen]
|
||||
opt-level = 3
|
||||
[profile.dev.package.syn]
|
||||
opt-level = 3
|
||||
[profile.dev.package.proc-macro2]
|
||||
opt-level = 3
|
||||
[profile.release.package.horizon-mod-api-gen]
|
||||
opt-level = 3
|
||||
[profile.release.package.syn]
|
||||
opt-level = 3
|
||||
[profile.release.package.proc-macro2]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,99 +1,77 @@
|
||||
<p align="center">
|
||||
<img src="docs/media/banner.webp" alt="Horizon: the game you know, rebuilt in Rust" width="100%">
|
||||
<img src="docs/media/banner.webp" alt="The Horizon website: the game you know, rebuilt in Rust" width="100%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/horizon-rust/horizon/actions/workflows/ci.yml"><img src="https://github.com/horizon-rust/horizon/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
||||
<img src="https://img.shields.io/badge/Minecraft-Java%2026.3-5D9C3F" alt="Minecraft Java 26.3">
|
||||
<img src="https://img.shields.io/badge/Rust-stable-B7410E?logo=rust&logoColor=white" alt="Rust">
|
||||
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
|
||||
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-833%20open-F0795A" alt="Player-visible gaps open"></a>
|
||||
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-2A2470" alt="Linux, macOS and Windows">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3B2F7E" alt="MIT license"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://playhorizon.dev"><b>Website</b></a> ·
|
||||
<a href="#download"><b>Download</b></a> ·
|
||||
<a href="https://playhorizon.dev/docs/"><b>Docs</b></a> ·
|
||||
<a href="https://discord.gg/playhorizon"><b>Discord</b></a> ·
|
||||
<a href="https://playhorizon.dev/docs/"><b>Documentation</b></a> ·
|
||||
<a href="https://github.com/horizon-rust/horizon/releases"><b>Release notes</b></a> ·
|
||||
<a href="ROADMAP.md"><b>Roadmap</b></a> ·
|
||||
<a href="#project-status"><b>Project status</b></a> ·
|
||||
<a href="#contributing"><b>Contributing</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
|
||||
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
|
||||
</p>
|
||||
|
||||
# Horizon
|
||||
|
||||
**Minecraft: Java Edition 26.3, rewritten in Rust.** Your worlds, the same rules and the same terrain as the Java game, checked against it by hundreds of differential tests, in one native program with no Java. Free, open source (MIT) and unofficial: you play it with your own copy of Java Edition.
|
||||
Minecraft: Java Edition 26.3, rewritten in Rust. Same worlds, same rules, same terrain, checked against the Java game by more than 300 differential harnesses. Free, open source and unofficial.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/readme/valley.webp" alt="A river valley with a plains village, savanna hills and a cherry grove, seen from above with a long view distance" width="100%">
|
||||
</p>
|
||||
|
||||
## What works today
|
||||
|
||||
Everything below is in the current release.
|
||||
|
||||
- **The game.** Survival, creative, hardcore and spectator; vanilla world generation in all three dimensions, mobs, villagers, raids, redstone, every command, the HUD and the recipe book. Vanilla saves open as they are, and worlds from 1.8 to 26.2 are upgraded.
|
||||
- **Multiplayer.** Join vanilla and Horizon servers (online mode included), open a world to LAN, or invite friends with a code from the launcher's Home page.
|
||||
- **Graphics.** Distant terrain up to 1,024 chunks, graphics presets, HDR and controllers, all off by default where they would change the vanilla look ([settings](https://playhorizon.dev/docs/settings)).
|
||||
- **Packs.** Resource packs, data packs and shader packs ([below](#mods-and-shader-packs)).
|
||||
- **Mods.** Content mods and native Rust mods, installed from the launcher ([below](#mods-and-shader-packs)).
|
||||
- **Servers.** Host one from the launcher, or run `horizon-server` on a VPS ([below](#host-a-server)).
|
||||
- **The launcher.** It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/readme/sunset.webp" alt="Sunset behind a cherry grove on a hill, with a river below"></td>
|
||||
<td width="50%"><img src="docs/media/readme/nether.webp" alt="A crimson forest in the Nether, with a piglin among the fungi"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/readme/survival.webp" alt="Survival in a village square: two villagers, flowers, the hotbar, health and hunger"></td>
|
||||
<td width="50%"><img src="docs/media/readme/multiplayer.webp" alt="Two players in a LAN world: the other player's name tag and the player list"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Nightly 2026-11-05 on a Mac mini (Apple M4 Pro, Metal): vanilla's textures, no mods or shader packs, render distance 32 with distant terrain on.</sub>
|
||||
|
||||
## Performance
|
||||
|
||||
**Horizon draws more frames per second than Java Edition, and loads faster, on the same machine, world and path.** `tools/perf_bench.py` runs each game in turn, from a fresh world (seed 1234), waiting for the terrain and then flying it over a village, a forest, an ocean and a mountain. Measured on 2026-10-29/30 on an idle Windows 11 machine (Ryzen 7 9800X3D, RTX 5080), Horizon v0.2.97 as the launcher installs it, 1280x720 in a window, VSync off, three games per figure. Frames per second, mean of the four scenes, with the slowest 1% of frames in brackets:
|
||||
|
||||
| Render distance | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
|
||||
| --- | --- | --- | --- |
|
||||
| 8 chunks | 3,289 (1,327) | 2,437 (640) | 2,375 (709) |
|
||||
| 16 chunks | 2,074 (598) | 1,661 (375) | 1,586 (432) |
|
||||
| 32 chunks | 1,207 (333) | 1,031 (176) | 879 (264) |
|
||||
|
||||
"Vanilla + mods" is Sodium, Lithium, FerriteCore and ImmediatelyFast. Horizon reaches the title screen in 2.9–3.2 s against vanilla's 7.5–7.7 s (the modded client's 10.8–11.1 s), loads a scene's chunks 2.9–3.8 times faster, and playing holds a steady working set of 1.1–1.7 GB against 1.9–2.9 GB. Every scene with its confidence interval and the run's load log: [the 2026-10-29 reports](benchmarks/perf/windows-9800x3d-5080-2026-10-29.md). Method, and how to measure your own machine: [docs/benchmark.md](docs/benchmark.md). [playhorizon.dev/benchmarks](https://playhorizon.dev/benchmarks) puts these runs beside a 2016 laptop, a Mac mini and a gaming desktop.
|
||||
> Not an official Minecraft product, nor approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own copy of Java Edition 26.3.
|
||||
|
||||
## Download
|
||||
|
||||
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev), or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true):
|
||||
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
|
||||
|
||||
| Platform | File | Needs |
|
||||
| --- | --- | --- |
|
||||
| Windows x64 | `horizon-launcher-setup.exe` | Windows 10 or 11, a GPU with Vulkan or DirectX 12 |
|
||||
| Windows on Arm | `horizon-launcher-setup-arm64.exe` | Windows 11 on Arm |
|
||||
| macOS, Apple Silicon | `horizon-launcher-macos-arm64.dmg` | macOS 12 or later |
|
||||
| macOS, Intel | `horizon-launcher-macos-x64.dmg` | macOS 12 or later |
|
||||
| Linux x86-64 | `horizon-launcher-linux-x86_64.AppImage` or `.tar.gz` | a glibc distribution, X11 or Wayland, a GPU with Vulkan or OpenGL 3.3 |
|
||||
|
||||
**You also need a Microsoft account that owns Minecraft: Java Edition.** You sign in with it in the launcher, which plays only with an account that owns the game ([why](docs/microsoft-account.md)). Horizon contains no Minecraft code or assets: an existing install (official launcher, Prism, MultiMC...) is reused, and without one the launcher downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
|
||||
**You need** a Microsoft account that owns Java Edition ([why](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 (Linux), Vulkan or DirectX 12 (Windows), or a Mac (Apple Silicon, or Intel from the next Nightly). An existing Minecraft install (official launcher, Prism, MultiMC...) is reused; without one, Play downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/readme/launcher-home.webp" alt="The launcher's Home page: the Horizon instance, Play, the version picker, the friend invite field, screenshots and the patch log"></td>
|
||||
<td width="50%"><img src="docs/media/readme/launcher-mods.webp" alt="The launcher's Mods page: Horizon's mod registry with Zoom selected, and tabs for modpacks, resource packs, shader packs and data packs"></td>
|
||||
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Home page: the selected instance ready to play, the version picker, Join a friend and the instance's tabs"></td>
|
||||
<td width="50%"><img src="docs/media/launcher-settings.webp" alt="The launcher's Settings page: Microsoft account, offline name, updates, appearance and accent colour"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Mods and shader packs
|
||||
## What works today
|
||||
|
||||
- **Content mods** (`.hmod`): blocks, items, recipes, loot, models and textures, with no code.
|
||||
- **Native mods** (`.hnmod`): Rust that the launcher compiles into the game for you, with nothing to install.
|
||||
- **Java mods** (Fabric, NeoForge, Forge) don't run: Horizon has no Java. The launcher carries over the data and assets inside their jars, and imports modpacks (`.mrpack`, CurseForge zips, Prism instances) the same way ([modpacks](https://playhorizon.dev/docs/modpacks)).
|
||||
- **Shader packs**: Iris/OptiFine-format packs (`shaders/` folders) and Horizon's own WGSL packs, under Video Settings → Shader Packs ([pack format](docs/shaders.md)).
|
||||
- **The game:** vanilla world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on vanilla saves. Worlds from 1.8 to 26.2 are upgraded. [Status](#project-status).
|
||||
- **Extras:** new menus, graphics presets, distant terrain up to 1024 chunks, controllers, zoom, minimap and more, off by default where they change the vanilla look. [Settings](https://playhorizon.dev/docs/settings).
|
||||
- **Packs:** resource packs, and data packs with functions, predicates, item modifiers and world generation.
|
||||
- **Multiplayer:** invite friends with a code, Open to LAN, join servers (online mode included). [Multiplayer](https://playhorizon.dev/docs/multiplayer).
|
||||
- **Mods:** content mods (blocks, items, data) and native Rust mods, built by the launcher with nothing to install. Java mod jars do not run. [Mods](https://playhorizon.dev/docs/mods).
|
||||
|
||||
Mods are off by default: a fresh install plays exactly like vanilla 26.3. The launcher's Mods page browses Horizon's registry and Modrinth. [Mods](https://playhorizon.dev/docs/mods) · [make a mod](docs/mods.md).
|
||||
In the Rust client, with vanilla assets and no mods or shaders:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<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>
|
||||
</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>
|
||||
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Host a server
|
||||
|
||||
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends. On a VPS, `horizon-server` installs with one command, needs no Java, and takes vanilla 26.3 and Horizon players alike. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
|
||||
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and needs no Java. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
|
||||
|
||||
## Modpacks
|
||||
|
||||
The launcher imports Modrinth and CurseForge packs and plays the mods Horizon can carry over: today, content mods made of data and assets, not Java code. [Modpacks](https://playhorizon.dev/docs/modpacks).
|
||||
|
||||
## Agent control
|
||||
|
||||
@@ -102,11 +80,13 @@ AI agents and scripts can play Horizon. `horizon-ctl`, shipped with the game, re
|
||||
```sh
|
||||
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
|
||||
horizon-ctl state # position, health, food, target, screen...
|
||||
horizon-ctl find-block birch_log # the nearest logs
|
||||
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
|
||||
horizon-ctl break -1 65 -3 # mines it; replies with the drops
|
||||
horizon-ctl open-inventory && horizon-ctl click-slot 0 # menus: slots, buttons by label, text fields
|
||||
```
|
||||
|
||||
`horizon-ctl help` lists every command. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
|
||||
Commands block until done and reply with the result; `horizon-ctl help` lists them all. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
|
||||
|
||||
## Build from source
|
||||
|
||||
@@ -125,9 +105,13 @@ cargo run --release -p horizon-client
|
||||
|
||||
Prepared files land in the gitignored `.local/`: never commit or share them. Options, controls and architecture: [the native client guide](docs/native-client.md); all developer docs: [docs/README.md](docs/README.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
What is in progress, what comes next and where to help: [ROADMAP.md](ROADMAP.md), or [playhorizon.dev/hub](https://playhorizon.dev/hub) with the work board and what shipped.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests), and see [ROADMAP.md](ROADMAP.md) or [playhorizon.dev/hub](https://playhorizon.dev/hub) for what's next. Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
|
||||
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
@@ -138,13 +122,13 @@ It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public
|
||||
|
||||
## Project status
|
||||
|
||||
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
|
||||
**Player-visible gaps: 833 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Screens, HUD and client packets [197](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [129](https://github.com/horizon-rust/horizon/issues/3519); Animals [110](https://github.com/horizon-rust/horizon/issues/3512); Server and players on the server [106](https://github.com/horizon-rust/horizon/issues/3520); Blocks, redstone and fluids [86](https://github.com/horizon-rust/horizon/issues/3516); Entities, projectiles, vehicles and decorations [64](https://github.com/horizon-rust/horizon/issues/3514); Monsters and bosses [53](https://github.com/horizon-rust/horizon/issues/3513); Mob AI, villagers and raids [35](https://github.com/horizon-rust/horizon/issues/3521); World, world generation and saves [32](https://github.com/horizon-rust/horizon/issues/3517); Items, enchantments and inventories [21](https://github.com/horizon-rust/horizon/issues/3515). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
|
||||
|
||||
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs. What players still run into: [known issues](docs/known-issues.md).
|
||||
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs.
|
||||
|
||||
### Java parity: 100%
|
||||
|
||||
Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75 (mostly complete) or 1 (complete and verified by differential tests); the percentage is their average. Last audited 2026-10-13: the harnesses pass on Linux and macOS. The [port ledger](tools/port_ledger.py) adds a method-by-method view of what is ported.
|
||||
Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75 (mostly complete) or 1 (complete and verified by differential tests); the percentage is their average. Last audited 2026-10-13: all 281 harnesses pass on Linux and macOS. The [port ledger](tools/port_ledger.py) adds a method-by-method view of what is ported.
|
||||
|
||||
| Subsystem | Score | Main gaps |
|
||||
| --- | ---: | --- |
|
||||
@@ -163,8 +147,14 @@ Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75
|
||||
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
|
||||
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000), container spills (4,000), copper golems and the sculk family (435 scenarios) match |
|
||||
|
||||
### Known issues
|
||||
|
||||
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
|
||||
|
||||
## Licence
|
||||
|
||||
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Horizon is made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)) and [its contributors](https://github.com/horizon-rust/horizon/graphs/contributors); support it on [GitHub Sponsors](https://github.com/sponsors/ludovic111).
|
||||
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
|
||||
|
||||
NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
|
||||
## Author
|
||||
|
||||
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
|
||||
@@ -206,7 +206,7 @@ Manage a server from a console, ban and pardon players, and drive it from other
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`horizon-server console` (a text interface), `ban`, `pardon`, `ban-ip` and `mcp` over stdio. Admin API additions: the player list, kick, ban, op and whitelist, an SSE stream of logs and events, a text metrics endpoint, config GET and PATCH, backup delete and download. The last pieces merged: bans edited while the server is stopped ([#9767](https://github.com/horizon-rust/horizon/issues/9767)) and the MCP tools' contracts pinned by a test ([#9748](https://github.com/horizon-rust/horizon/issues/9748)). The panel's later features and vanilla's management protocol are their own item (server-panel-management). [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
|
||||
`horizon-server console` (a text interface), `ban`, `pardon`, `ban-ip` and `mcp` over stdio. Admin API additions: the player list, kick, ban, op and whitelist, an SSE stream of logs and events, a text metrics endpoint, config GET and PATCH, backup delete and download. The panel's later features (bans, worlds, server.properties import, logs and crash reports, modpacks) and vanilla's management protocol (the JSON-RPC over WebSocket that the `management-server-*` keys configure), so vanilla's tools can manage a Horizon server. [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -266,7 +266,7 @@ Shader packs were removed and are being rebuilt from scratch. They come back, of
|
||||
|
||||
A clean-room rewrite in `horizon-shaderpack`, from the packs' public format documentation only: reading packs, the preprocessor and `.properties` files are merged ([#1584](https://github.com/horizon-rust/horizon/issues/1584), [#1585](https://github.com/horizon-rust/horizon/issues/1585), [#1586](https://github.com/horizon-rust/horizon/issues/1586)); translating programs to WGSL through naga, buffers, uniforms and the terrain, entity, sky, weather and hand stages are in review ([#1698](https://github.com/horizon-rust/horizon/issues/1698) to [#1801](https://github.com/horizon-rust/horizon/issues/1801)).
|
||||
|
||||
More: [#1561](https://github.com/horizon-rust/horizon/issues/1561) · [#1771](https://github.com/horizon-rust/horizon/issues/1771) · [#1786](https://github.com/horizon-rust/horizon/issues/1786) · [#9045](https://github.com/horizon-rust/horizon/issues/9045) · [#6233](https://github.com/horizon-rust/horizon/issues/6233) · [#6485](https://github.com/horizon-rust/horizon/issues/6485) · [#3734](https://github.com/horizon-rust/horizon/issues/3734) · [#3125](https://github.com/horizon-rust/horizon/issues/3125)
|
||||
More: [#1561](https://github.com/horizon-rust/horizon/issues/1561) · [#1771](https://github.com/horizon-rust/horizon/issues/1771) · [#1786](https://github.com/horizon-rust/horizon/issues/1786)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -298,8 +298,6 @@ Far terrain drawn from the world you really built and explored, caves and builds
|
||||
|
||||
Keep a 3D mip store on disk built from real chunks, drawn by the GPU beside the seed preview for areas nobody has visited. Written from scratch and measured before it is kept. Builds on Far Detail and the far-terrain levels of detail already shipped; the open work on them is [#718](https://github.com/horizon-rust/horizon/issues/718).
|
||||
|
||||
More: [#4678](https://github.com/horizon-rust/horizon/issues/4678)
|
||||
|
||||
</details>
|
||||
|
||||
#### A batch of targeted optimisations
|
||||
@@ -310,7 +308,7 @@ Several smaller speed-ups, each measured: idle block entities that sleep, cheape
|
||||
|
||||
Block entities and hoppers that skip work when idle (the hopper item scan is already skipped when no item is near); a particle frustum test first (also a parity gap), then particle quads built on the GPU; jigsaw shortcuts that skip blocked connectors and pool elements known not to fit, with layouts unchanged (trial chambers and ancient cities first); batched raw mouse input on Windows for 8000 Hz mice; a better atlas packer for huge resource packs (a 64x pack builds a 704 MiB atlas for 452 MiB of textures); sign-heavy scenes ([#1461](https://github.com/horizon-rust/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
|
||||
|
||||
More: [docs/profiling.md](docs/profiling.md) · [#7752](https://github.com/horizon-rust/horizon/issues/7752) · [#8251](https://github.com/horizon-rust/horizon/issues/8251) · [#10229](https://github.com/horizon-rust/horizon/issues/10229)
|
||||
More: [docs/profiling.md](docs/profiling.md)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -324,8 +322,6 @@ Recognise mods built for versions before 26.1 in the mod browser, so they can be
|
||||
|
||||
A mapping table from intermediary and SRG names so pre-26.1 jars can be classified, and a rename for mod ids with dashes (`fabric-api`); the port step then treats them like newer jars ([docs/mod-browser.md](docs/mod-browser.md)).
|
||||
|
||||
More: [#10262](https://github.com/horizon-rust/horizon/issues/10262)
|
||||
|
||||
</details>
|
||||
|
||||
#### A modpack's own resource and shader packs
|
||||
@@ -400,13 +396,13 @@ A Datapacks tab beside Mods and Resource packs: install into a chosen world's da
|
||||
|
||||
### Servers and multiplayer
|
||||
|
||||
#### Server panel extras and vanilla's management protocol
|
||||
#### Ely.by and other sign-in providers
|
||||
|
||||
More of a server managed from its web panel (bans, worlds, logs and crash reports, modpacks), and vanilla's own server-management tools working with a Horizon server.
|
||||
Sign in with other account providers, such as Ely.by or any Yggdrasil-compatible one, beside Microsoft.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
The panel's later features (bans, worlds, server.properties import, logs and crash reports, modpacks) and vanilla's management protocol (the JSON-RPC over WebSocket that the `management-server-*` keys configure), so vanilla's tools can manage a Horizon server. Split from server-admin. [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
|
||||
Issue [#1270](https://github.com/horizon-rust/horizon/issues/1270) asks for Ely.by and Yggdrasil-compatible servers with an account selector in the launcher. It builds on the Microsoft sign-in ([docs/microsoft-account.md](docs/microsoft-account.md)).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -530,8 +526,6 @@ Contraptions and physics like the Create and Aeronautics mods, as a mod for Hori
|
||||
|
||||
Issue [#805](https://github.com/horizon-rust/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod.
|
||||
|
||||
More: [#6302](https://github.com/horizon-rust/horizon/issues/6302) · [#5313](https://github.com/horizon-rust/horizon/issues/5313) · [#5556](https://github.com/horizon-rust/horizon/issues/5556)
|
||||
|
||||
</details>
|
||||
|
||||
#### Proximity voice chat as a native mod
|
||||
@@ -554,8 +548,6 @@ Every past and future Minecraft version prepared for Horizon automatically. When
|
||||
|
||||
The port pipeline ([docs/version-ports.md](docs/version-ports.md), `tools/version_port.py`) detects a release, prepares it, diffs its classes against the pinned 26.3, regenerates the data-driven tables, runs every parity harness on it and writes a port report that becomes issues. The goal is as little human work as possible for any version, past or future. Detection and the first slices are merged; the scheduled agent that runs it every day isn't installed yet. Two versions in one build are out of scope.
|
||||
|
||||
More: [#4758](https://github.com/horizon-rust/horizon/issues/4758)
|
||||
|
||||
</details>
|
||||
|
||||
#### Controllers: tested on real pads, and in the launcher
|
||||
@@ -566,8 +558,6 @@ Make controller support solid: tested with real PlayStation and Xbox pads and a
|
||||
|
||||
The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read with gilrs, menus, rumble, glyphs, remapping and DualSense adaptive triggers, lightbar and haptics, but not yet confirmed on real pads or a Steam Deck (DualSense haptics stay behind a switch until then). Left: a test pass on real hardware, and the same focus navigation in the GPUI launcher.
|
||||
|
||||
More: [#10298](https://github.com/horizon-rust/horizon/issues/10298) · [#8678](https://github.com/horizon-rust/horizon/issues/8678)
|
||||
|
||||
</details>
|
||||
|
||||
#### The full debug screen
|
||||
@@ -610,6 +600,16 @@ An output device list in Music & Sounds, falling back to the system default. [#1
|
||||
|
||||
</details>
|
||||
|
||||
#### Play in your language
|
||||
|
||||
Item names, container titles and server messages follow the language you pick, as in vanilla. Today parts of the game stay in English.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Translatable components resolved through the selected language for items, containers and server text ([#1806](https://github.com/horizon-rust/horizon/issues/1806)), on top of the bundled languages the prepared resources carry ([#1594](https://github.com/horizon-rust/horizon/issues/1594)). Left: the client's own screens ([#1672](https://github.com/horizon-rust/horizon/issues/1672)) and the main menu with zh_cn.
|
||||
|
||||
</details>
|
||||
|
||||
### Rendering
|
||||
|
||||
#### A light default shader pack
|
||||
@@ -668,16 +668,6 @@ A shared resource store keyed by the preparer's digest, no prepare intermediates
|
||||
|
||||
</details>
|
||||
|
||||
#### A launcher screen readers can read
|
||||
|
||||
Screen readers read the launcher's buttons, lists and settings, and every part of it works from the keyboard.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#4537](https://github.com/horizon-rust/horizon/issues/4537): the launcher is GPUI (not Electron), so it exposes nothing to UI Automation, NSAccessibility or AT-SPI yet. Needs an accessibility tree from GPUI (an AccessKit bridge) and a role and label on each widget.
|
||||
|
||||
</details>
|
||||
|
||||
#### A visible beta label
|
||||
|
||||
A clear 'beta' label in the launcher and the game, so nobody is surprised that Horizon is still being built.
|
||||
@@ -700,18 +690,6 @@ Issue [#1709](https://github.com/horizon-rust/horizon/issues/1709): aarch64 buil
|
||||
|
||||
</details>
|
||||
|
||||
#### The launcher builds Horizon on other systems
|
||||
|
||||
On a system Horizon has no download for, such as FreeBSD, the launcher offers to build the game on your computer, as it already does for native mods.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#4354](https://github.com/horizon-rust/horizon/issues/4354): when no release asset matches the platform, the launcher fetches the tagged source and runs the same pinned-toolchain `cargo build --locked` its native-mod builds use, with a warning about the time it takes. The build is unsigned, so the launcher marks the version as built locally.
|
||||
|
||||
More: [docs/native-mods.md](docs/native-mods.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Horizon on Windows ARM64
|
||||
|
||||
A native Horizon for Windows on ARM computers, such as Snapdragon laptops.
|
||||
@@ -732,8 +710,6 @@ Versions of Horizon for Android, iOS and web browsers. Nothing official yet; con
|
||||
|
||||
No official build. Android was first closed as not planned, with touch controls, a mobile interface, packaging and loading the player's own assets all missing; the roadmap now includes it, iOS and a build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/horizon-rust/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/horizon-rust/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/horizon-rust/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
|
||||
|
||||
More: [#9134](https://github.com/horizon-rust/horizon/issues/9134) · [#8069](https://github.com/horizon-rust/horizon/issues/8069) · [#4153](https://github.com/horizon-rust/horizon/issues/4153)
|
||||
|
||||
</details>
|
||||
|
||||
#### Horizon on Intel Macs
|
||||
@@ -776,8 +752,6 @@ Record, replay and edit videos without leaving the game: record in a keypress, r
|
||||
|
||||
Three stages. Recording: capture gameplay in a keypress with an encoder thread and little frame-time cost. Replay: a replay mode for recorded sessions. Editing: cuts, camera paths, titles and export. None of it exists yet; the agent-control socket ([docs/agent-control.md](docs/agent-control.md)) already lets tools drive the game for scripted shots.
|
||||
|
||||
More: [#10025](https://github.com/horizon-rust/horizon/issues/10025)
|
||||
|
||||
</details>
|
||||
|
||||
#### Game menus in the launcher's style
|
||||
@@ -868,8 +842,6 @@ Every download Horizon makes is checked against the project's signature, includi
|
||||
|
||||
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/horizon-rust/horizon/issues/1011), [#1014](https://github.com/horizon-rust/horizon/issues/1014)). Left: the first install through `install.sh` and `install.ps1` still trusts only the checksum GitHub publishes, because verifying Ed25519 signatures needs OpenSSL 3, which macOS doesn't ship ([docs/security.md](docs/security.md)).
|
||||
|
||||
More: [#10354](https://github.com/horizon-rust/horizon/issues/10354)
|
||||
|
||||
</details>
|
||||
|
||||
## Help wanted
|
||||
@@ -888,11 +860,30 @@ More: [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/pe
|
||||
|
||||
</details>
|
||||
|
||||
### Gameplay parity
|
||||
|
||||
#### End city banners keep their pattern
|
||||
|
||||
Banners in End cities show as plain magenta instead of with their pattern.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Compare the banner block entities the End city templates place with vanilla's (`EndCityPieces`, the templates' block entity NBT, `BannerBlockEntity`'s patterns), in `crates/horizon-world/src/worldgen/structure/`. Issue [#1438](https://github.com/horizon-rust/horizon/issues/1438); possibly related: [#526](https://github.com/horizon-rust/horizon/issues/526).
|
||||
|
||||
</details>
|
||||
|
||||
#### Enchanted loot in End cities
|
||||
|
||||
Armour and tools in End city chests should come enchanted, as in vanilla.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`loot.rs` implements `enchant_with_levels`: find why the End city chest table's rolls come out bare, and compare them with vanilla's for a few seeds. Issues [#1437](https://github.com/horizon-rust/horizon/issues/1437), [#1660](https://github.com/horizon-rust/horizon/issues/1660).
|
||||
|
||||
</details>
|
||||
|
||||
## Recently shipped
|
||||
|
||||
- **Enchanted loot in End cities** (v0.2.73): Armour and tools in End city chests should come enchanted, as in vanilla. [#1437](https://github.com/horizon-rust/horizon/issues/1437) · [#1660](https://github.com/horizon-rust/horizon/issues/1660) · [#1911](https://github.com/horizon-rust/horizon/issues/1911) · [#2037](https://github.com/horizon-rust/horizon/issues/2037)
|
||||
- **End city banners keep their pattern** (v0.2.67): Banners in End cities show as plain magenta instead of with their pattern. [#1438](https://github.com/horizon-rust/horizon/issues/1438) · [#526](https://github.com/horizon-rust/horizon/issues/526) · [#1928](https://github.com/horizon-rust/horizon/issues/1928)
|
||||
- **Play in your language** (v0.2.65): Item names, container titles and server messages follow the language you pick, as in vanilla. Today parts of the game stay in English. [#1806](https://github.com/horizon-rust/horizon/issues/1806) · [#1594](https://github.com/horizon-rust/horizon/issues/1594) · [#1672](https://github.com/horizon-rust/horizon/issues/1672)
|
||||
- **Chickens flap while they fall** (v0.2.67): Chickens should flap their wings while they fall, not stand still in the air. [#1706](https://github.com/horizon-rust/horizon/issues/1706) · [#1892](https://github.com/horizon-rust/horizon/issues/1892)
|
||||
- **Ctrl+Backspace and scroll bars in menus** (v0.2.67): Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable. [#1764](https://github.com/horizon-rust/horizon/issues/1764) · [#1782](https://github.com/horizon-rust/horizon/issues/1782) · [#1884](https://github.com/horizon-rust/horizon/issues/1884) · [#1931](https://github.com/horizon-rust/horizon/issues/1931)
|
||||
- **A pointing hand over buttons** (v0.2.67): The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3. [#1347](https://github.com/horizon-rust/horizon/issues/1347) · [#1897](https://github.com/horizon-rust/horizon/issues/1897)
|
||||
|
||||
@@ -1,56 +1,12 @@
|
||||
# Third-party notices
|
||||
|
||||
## Code removed for its license
|
||||
|
||||
Horizon once shipped three pieces of code under MIT that came from projects whose licenses don't allow that. All
|
||||
three are gone from main. They are listed here for anyone who holds an older commit, tag or release: in those
|
||||
versions, that code is not covered by Horizon's MIT license. It stays under its authors' copyright and its original
|
||||
license.
|
||||
|
||||
- **Shader packs written from Iris's source.** `crates/horizon-shaderpack` and the render stages that used it were
|
||||
written from the source of [Iris](https://github.com/IrisShaders/Iris) (LGPL-3.0, copyright the Iris contributors).
|
||||
They were added from 15 October 2026 (`b22583d057`), shipped in the game releases `v2026.10.16.02` to `v0.2.46`,
|
||||
and were removed on 24 October 2026 ([#1561](https://github.com/horizon-rust/horizon/pull/1561)) after a developer
|
||||
of Iris showed us. Shader packs came back in `crates/horizon-shaders`, first written from a description of the
|
||||
pack format.
|
||||
- **Math functions translated from OpenJDK.** In `crates/horizon-core/src/jmath.rs`, `fdlibm::{log, log10, cbrt,
|
||||
hypot}` were translated from OpenJDK's `java/lang/FdLibm.java` and `round_f32` from its `java/lang/Math.java`
|
||||
(GPL-2.0 with the Classpath Exception), and `aarch64::{sin, cos}` from HotSpot's `macroAssembler_aarch64_trig.cpp`
|
||||
(GPL-2.0); copyright Oracle and/or its affiliates, and Cavium for that file. The `fdlibm` and `aarch64` functions
|
||||
were added on 13 October 2026 (`558d140fca`, first released in `v2026.10.13`). `round_f32` was added on 14 October
|
||||
2026 as `java_round` in `crates/horizon-client/src/mipmap.rs` (`95db0fce9d`) and moved into `jmath.rs` on 3
|
||||
November ([#7770](https://github.com/horizon-rust/horizon/pull/7770)). All of them were replaced in
|
||||
[#11944](https://github.com/horizon-rust/horizon/pull/11944) by code written without OpenJDK's sources, from Sun's
|
||||
fdlibm C sources, the Java SE API documentation and a description of the behaviour, and checked against a JVM
|
||||
([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
|
||||
- **Java's `HashMap` translated from OpenJDK.** `crates/horizon-world/src/java_hash_map.rs`, which reproduces the
|
||||
iteration order of `java.util.HashMap`, was translated from OpenJDK's `java/util/HashMap.java` (GPL-2.0 with the
|
||||
Classpath Exception; copyright Oracle and/or its affiliates). It was added on 7 October 2026 (`118e93ea51` and
|
||||
`194db0dbb4`, first released in `v2026.10.12`). It was replaced in
|
||||
[#12043](https://github.com/horizon-rust/horizon/pull/12043) by code written without OpenJDK's sources, from a
|
||||
description of the behaviour, the Java SE API documentation and the textbook red-black tree, and checked against a
|
||||
JVM ([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
|
||||
|
||||
## Catnip / Ponder
|
||||
|
||||
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
|
||||
UV rules of Catnip's `FluidRenderHelper` from Ponder 1.0.82 for Minecraft 1.21.1
|
||||
(`net.createmod.ponder:ponder-neoforge:1.0.82+mc1.21.1`). Copyright (c) 2022 The Create Team,
|
||||
licensed under MIT (`LICENSE_Ponder` in that artifact); the full license heads the Rust file.
|
||||
The native renderer uses Horizon's own block-sprite arrays, pose and light packing. The
|
||||
development-only oracle calls the unchanged helper from the player's local artifact. No
|
||||
original classes, game shaders, textures or prepared resources are distributed here.
|
||||
|
||||
## wgpu-hal
|
||||
|
||||
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
|
||||
developers, with upstream fixes applied
|
||||
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124),
|
||||
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). Its GL backend also reports
|
||||
the context's sampled textures and samplers per shader stage (usually 32 on desktop GL,
|
||||
where WebGL2's minimum 16 was reported before), so a pipeline layout the driver accepts is
|
||||
not refused. It is licensed under MIT or Apache-2.0; both licenses are in that folder
|
||||
(`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). It is licensed under MIT or
|
||||
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
|
||||
## naga
|
||||
|
||||
@@ -58,11 +14,6 @@ not refused. It is licensed under MIT or Apache-2.0; both licenses are in that f
|
||||
fixes to its GLSL frontend that shader packs need, each applied in the commit that needed it. It is licensed under MIT or Apache-2.0; both licenses
|
||||
are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
|
||||
The GLSL frontend also resets the common type for each comma-separated declarator so one
|
||||
variable's array specifier does not change later variables.
|
||||
It also reads a struct member outside a constant expression through a pointer to the member,
|
||||
not out of a loaded copy of the whole struct, so a member an atomic function uses can be read.
|
||||
|
||||
## libmimalloc-sys and mimalloc
|
||||
|
||||
`third_party/libmimalloc-sys` is [libmimalloc-sys](https://github.com/purpleprotocol/mimalloc_rust)
|
||||
@@ -214,98 +165,3 @@ BlueMap is licensed under the MIT license:
|
||||
> 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.
|
||||
|
||||
## JOML
|
||||
|
||||
`crates/horizon-client-render/src/camera.rs` (`joml::Mat4::rotation`, `invert_orthonormal`) and
|
||||
`crates/horizon-client-render/src/gfx/pack_uniforms.rs` (`invert_generic`, `invert_affine`, the
|
||||
formulas of `pack_projection` and `shadow_ortho`) adapt the float arithmetic of
|
||||
`Matrix4f.rotation(Quaternionfc)`, `invertOrthonormal`, `invertGenericNonThis`, `invertAffine`,
|
||||
`setPerspective` and `setOrtho` from
|
||||
[JOML](https://github.com/JOML-CI/JOML/tree/faf9eef2511242122beb6055fe3da92daed214c5) 1.10.9, the
|
||||
library vanilla 26.3 uses, so that shader packs get the same matrices to the last bit. Changes:
|
||||
ported to Rust; the pack_uniforms.rs functions drop JOML's property flags, and
|
||||
`pack_projection` works the formula in doubles. The copyright line and license notice also head
|
||||
both files.
|
||||
|
||||
`camera.rs` also adapts `Quaternionf.rotationX`, `rotationY` and `rotationZ`,
|
||||
`Matrix4f.rotateAffine` and `Vector4f.mulAffine` (`joml::Quat::rotation_x`/`_y`/`_z`,
|
||||
`joml::Mat4::rotate_affine`, `transform_affine`), for the sky's positions shader packs read.
|
||||
Changes: ported to Rust; `rotate_affine` keeps only the affine case.
|
||||
|
||||
```
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2015-2026 Richard Greenlees
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Just Enough Items
|
||||
|
||||
The Recipes GUI sprites and settings icon under `crates/horizon-client-render/src/jei_recipes` are from
|
||||
[Just Enough Items 31.17.0.108](https://github.com/mezz/JustEnoughItems/tree/0371fa6fafa14635a356019e4604f5580b26a34f)
|
||||
by mezz and contributors, under the MIT license in that directory (`LICENSE.txt`).
|
||||
|
||||
## Flywheel native model directional shading
|
||||
|
||||
The two-light diffuse expression and transformed-normal policy in
|
||||
`terrain.wgsl`'s native model entries follow Flywheel 1.0.6
|
||||
(`internal/diffuse.glsl`, `instance/transformed.vert`, `internal/common.vert`).
|
||||
No Flywheel classes, compiled shaders or original model/texture assets are bundled.
|
||||
|
||||
Copyright (c) 2021-2024 Jozufozu
|
||||
|
||||
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.
|
||||
|
||||
## fdlibm
|
||||
|
||||
`crates/horizon-core/src/jmath/fdlibm.rs` and the `fdlibm_asin`, `fdlibm_atan` and
|
||||
`fdlibm_atan2` functions in `crates/horizon-core/src/geometry/trig.rs` are Rust ports of
|
||||
[fdlibm](https://www.netlib.org/fdlibm/) 5.3, Sun's freely distributable math library, whose
|
||||
results the Java SE API specifies for `StrictMath`. `fdlibm.rs` ports `e_log.c`, `e_log10.c`,
|
||||
`s_cbrt.c` and `e_hypot.c`, and, with fused multiply-adds, `s_sin.c`, `s_cos.c`, `k_sin.c`,
|
||||
`k_cos.c` and `e_rem_pio2.c`; `trig.rs` ports `e_asin.c`, `s_atan.c` and `e_atan2.c`. This
|
||||
notice also heads both files:
|
||||
|
||||
```
|
||||
Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
|
||||
|
||||
Developed at SunSoft, a Sun Microsystems, Inc. business.
|
||||
Permission to use, copy, modify, and distribute this
|
||||
software is freely granted, provided that this notice
|
||||
is preserved.
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Server load bench: the same horizon-bots load on Horizon, vanilla 26.3 or Pumpkin.
|
||||
|
||||
python3 tools/bench.py --server horizon --exe target/release/horizon-server --bots 50
|
||||
python3 tools/bench.py --server vanilla --jar ~/mc/server.jar --bots 50 --pre-generated ~/worlds/seed12345
|
||||
python3 tools/bench.py --server pumpkin --exe ./pumpkin --bots 50
|
||||
python3 bench/bench.py --server horizon --exe target/release/horizon-server --bots 50
|
||||
python3 bench/bench.py --server vanilla --jar ~/mc/server.jar --bots 50 --pre-generated ~/worlds/seed12345
|
||||
python3 bench/bench.py --server pumpkin --exe ./pumpkin --bots 50
|
||||
|
||||
Every run uses a fresh scratch folder (bound to 127.0.0.1, offline, creative so the bots can fly), starts the
|
||||
server, lets `horizon-bots` join one every --join-every seconds, warms up, then measures for --seconds:
|
||||
@@ -39,11 +39,6 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
from . import lab
|
||||
except ImportError:
|
||||
import lab
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
EXE = '.exe' if os.name == 'nt' else ''
|
||||
SERVERS = ('horizon', 'vanilla', 'pumpkin')
|
||||
@@ -212,7 +207,8 @@ class Server:
|
||||
self.lines = []
|
||||
self.lock = threading.Lock()
|
||||
self.log = log_path.open('w', encoding='utf-8')
|
||||
env = lab.game_env(HORIZON_HOME=directory / 'horizon-home')
|
||||
env = dict(os.environ, HORIZON_NO_AUDIO='1', HORIZON_HOME=str(directory / 'horizon-home'),
|
||||
HORIZON_LAUNCHER_NO_SELF_UPDATE='1')
|
||||
self.process = subprocess.Popen([str(c) for c in command], cwd=directory, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
encoding='utf-8', errors='replace', env=env)
|
||||
@@ -1,42 +0,0 @@
|
||||
# Entity occlusion split measurements — 2026-11-02
|
||||
|
||||
These measurements came from the pre-split integration branch for PR #7054. That build contained the entity-occlusion rewrite and particle light lookup cache; Particle Culling was Off. They are useful comparison evidence, but they are not a benchmark of this isolated entity-only commit. The cache-only run in PR #7391 measured effectively the same CPU time on the ordinary scene, but the combined result still cannot isolate interactions.
|
||||
|
||||
Host: Intel Core i7-3770S, Intel HD Graphics 4000, Vulkan/Mesa, 675×727, render distance 4, VSync off, 260 FPS cap. Two opposite-order passes measured for 8 seconds after warmup. Values are means. CPU is harness average CPU ms per frame; p99 is frame time. The driver supplied no usable GPU timing.
|
||||
|
||||
## Ordinary scene
|
||||
|
||||
The scene had 24 mobs, roughly 9,400–10,100 rain particles and campfire smoke.
|
||||
|
||||
| Build and mode | FPS | CPU ms/frame | Frame p99 ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Main, entity culling Off | 88.59 | 1.829 | 24.51 |
|
||||
| Pre-split integration branch, all culling Off | 88.33 | 2.063 | 24.51 |
|
||||
| Main, entity culling Synchronous | 89.34 | 1.965 | 23.99 |
|
||||
| Pre-split integration branch, entity Synchronous | 89.13 | 2.113 | 24.15 |
|
||||
|
||||
This normal-scene run does not show a large FPS change; its CPU differences are small relative to frame time and not attributable to the entity-only change by themselves.
|
||||
|
||||
## Opaque-wall scene
|
||||
|
||||
A 32×32 platform at Y=300, a 7-block wall with a one-block vertical opening, and 192 pigs spawned behind the wall (216 mobs in the captured scene). The player looked toward the opening from the near side. The same world and camera path were used for the main and integration builds.
|
||||
|
||||
| Build and mode | FPS | CPU ms/frame | Frame p99 ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Main, Off | 89.08 | 2.423 | 13.52 |
|
||||
| Integration branch, Off | 89.23 | 2.355 | 13.27 |
|
||||
| Main, Synchronous | 92.81 | 2.566 | 14.60 |
|
||||
| Integration branch, Synchronous | 93.21 | 2.550 | 12.84 |
|
||||
| Main, Asynchronous | 92.73 | 2.364 | 13.04 |
|
||||
| Integration branch, Asynchronous | 92.62 | 2.314 | 13.18 |
|
||||
|
||||
The asynchronous mode reduced measured CPU time slightly in this wall scene but did not improve FPS beyond run-to-run spread. This is a synthetic occlusion-heavy scene, not a broad performance claim.
|
||||
|
||||
## Captures
|
||||
|
||||
The HUD performance overlay is visible in each capture. The integration-build screenshots were recorded with the same wall and camera position in Off, Synchronous, and Asynchronous modes.
|
||||
|
||||
- Main, Off: `main-off.png`
|
||||
- Integration branch, Off: `branch-off.png`
|
||||
- Integration branch, Synchronous: `branch-sync.png`
|
||||
- Integration branch, Asynchronous: `branch-async.png`
|
||||
|
Before Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 398 KiB |
@@ -1,42 +0,0 @@
|
||||
# Entity occlusion pipeline — release/OpenGL run (2026-11-04)
|
||||
|
||||
This is the final local smoke benchmark for the entity-occlusion pipeline before opening the PR. It was run from the branch after the release build completed, with the OpenGL backend selected explicitly. The same process created each scene, switched the culling mode, flew the camera for two seconds, collected frame/CPU telemetry, and captured the screenshot after the scene settled.
|
||||
|
||||
**Host:** Intel Core i7-3770S, Intel HD Graphics 4000, Mesa OpenGL 4.2
|
||||
**Window:** 1280×720, offscreen, VSync disabled
|
||||
**World:** fresh temporary home, 32×32 platform at Y=300; wall scenes add a solid stone wall and an optional one-block slit
|
||||
**Entities:** 24 pigs in the normal scene, 192 pigs in wall scenes, 512 pigs in stress scenes
|
||||
**Settings:** render distance 8, `entityCullingPrecheck:64`, FPS overlay on, no resource-pack occlusion assumptions
|
||||
**Build:** `cargo build --release -p horizon-client`
|
||||
|
||||
## Results
|
||||
|
||||
The values below are branch-only measurements. They are useful for comparing the three modes under identical geometry and for checking that the worker is doing useful work. A fresh main binary was not built in this short pass, so these numbers do not claim a controlled main-to-branch delta.
|
||||
|
||||
| Scene | Off FPS / CPU p50 | Sync FPS / CPU p50 | Async FPS / CPU p50 | Async entities culled |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| `entity_normal` (24 visible) | 240.5 / 0.586 ms | 233.5 / 0.762 ms | 239.3 / 0.686 ms | 0 |
|
||||
| `entity_wall` (192, solid) | 228.8 / 1.245 ms | 318.6 / 1.342 ms | **324.5 / 0.478 ms** | 124,608 |
|
||||
| `entity_wall_slit` (192, one-block slit) | 231.4 / 1.102 ms | 265.0 / 1.387 ms | **281.2 / 0.716 ms** | 102,826 |
|
||||
| `entity_stress` (512, solid) | 190.3 / 2.467 ms | 299.2 / 2.593 ms | **325.5 / 0.691 ms** | 290,234 |
|
||||
| `entity_stress_slit` (512, one-block slit) | 190.4 / 2.476 ms | 262.2 / 2.573 ms | **262.1 / 1.297 ms** | 227,301 |
|
||||
|
||||
Async telemetry confirmed that the render thread stayed non-blocking (`try_recv`/`try_send`, `worker_blocking_wait_ms: 0`). The latest wall/slit run completed 562 worker jobs, queued 3,982 queries, avoided about 4.0 million entity bytes, and had 379 ns recent p50 worker-job time. The solid 512-pig run culled 15,552 generated vertices; the slit run culled 120,290 generated vertices. The normal scene recorded no pre-model hides and kept the opt-in path close to Off.
|
||||
|
||||
## Captures
|
||||
|
||||
Every image includes the FPS performance overlay. The names are `<mode>-<scene>.png`:
|
||||
|
||||
- [Off — normal](screenshots/off-entity_normal.png), [wall](screenshots/off-entity_wall.png), [wall slit](screenshots/off-entity_wall_slit.png), [stress](screenshots/off-entity_stress.png), [stress slit](screenshots/off-entity_stress_slit.png)
|
||||
- [Sync — normal](screenshots/sync-entity_normal.png), [wall](screenshots/sync-entity_wall.png), [wall slit](screenshots/sync-entity_wall_slit.png), [stress](screenshots/sync-entity_stress.png), [stress slit](screenshots/sync-entity_stress_slit.png)
|
||||
- [Async — normal](screenshots/async-entity_normal.png), [wall](screenshots/async-entity_wall.png), [wall slit](screenshots/async-entity_wall_slit.png), [stress](screenshots/async-entity_stress.png), [stress slit](screenshots/async-entity_stress_slit.png)
|
||||
|
||||
## Reproduction
|
||||
|
||||
From the repository root, with prepared 26.3 resources available:
|
||||
|
||||
```sh
|
||||
python3 tools/entity_occlusion_bench.py
|
||||
```
|
||||
|
||||
The script uses fresh temporary game data, the release binary in `target/release`, OpenGL, the FPS overlay, and writes JSON plus captures under `/tmp/horizon-entity-occlusion-release-quick/`.
|
||||
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 206 KiB |
@@ -1,27 +0,0 @@
|
||||
# Particle culling review measurements — 2026-11-02
|
||||
|
||||
Ordinary-scene captures and measurements for PR #7054. The scene contains 24 mobs, about 9,400–10,100 rain particles, and about 50–100 campfire smoke particles. Two opposite-order passes were run with 2 s warmup and 8 s measurement per condition. Host: Intel Core i7-3770S, Intel HD Graphics 4000, Vulkan/Mesa, 675×727, render distance 4, VSync off, 260 FPS cap.
|
||||
|
||||
These are means of the two samples. CPU is the harness's average CPU time per frame; p99 is frame time. The normal scene is not expected to show large gains because most particles are visible.
|
||||
|
||||
| Condition | FPS | CPU ms/frame | Frame p99 ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Main, culling Off | 88.39 | 1.930 | 23.85 |
|
||||
| Particle branch, culling Off | 88.55 | 1.828 | 24.88 |
|
||||
| Main, Entity Culling Synchronous | 89.12 | 1.887 | 23.07 |
|
||||
| Particle branch, Entity Culling Synchronous, Particle Culling Off | 89.06 | 1.897 | 25.18 |
|
||||
| Particle Culling Synchronous | 88.87 | 1.774 | 24.94 |
|
||||
| Particle Culling Asynchronous* | 89.00 | 1.754 | 24.90 |
|
||||
|
||||
*The asynchronous samples predate the final switch to a dedicated particle worker. They are retained only as historical context and are not a measurement of the final worker implementation. No FPS improvement is claimed from these ordinary-scene runs. The Off path delegates directly to `render`; `disabled_culled_render_matches_the_regular_particle_path` checks that it produces the same vertices.
|
||||
|
||||
The FPS and CPU differences are within run-to-run variability for this GPU-limited scene. The first four conditions are the reviewer's requested Main-versus-branch default and existing synchronous-mode comparisons. Particle Sync/Async rows are branch-internal mode comparisons.
|
||||
|
||||
## Captures
|
||||
|
||||
The screenshots show the same ordinary test world and FPS overlay:
|
||||
|
||||
- `main-off.png`
|
||||
- `particle-off.png`
|
||||
- `particle-sync.png`
|
||||
- `particle-async.png`
|
||||
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 341 KiB |
@@ -1,870 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T13:00:15Z",
|
||||
"app": {
|
||||
"name": "horizon-client",
|
||||
"version": "nightly-2026-11-04-67dad0d",
|
||||
"commit": "67dad0d"
|
||||
},
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "x86_64",
|
||||
"cpu_threads": 24,
|
||||
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
|
||||
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
|
||||
"ram_mb": 31904,
|
||||
"gpu": {
|
||||
"name": "NVIDIA GeForce RTX 4080",
|
||||
"vendor": "nvidia",
|
||||
"driver": "NVIDIA 595.71.05",
|
||||
"backend": "vulkan",
|
||||
"kind": "discrete"
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 12,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": false,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 28.408,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 66658.183,
|
||||
"stdev": 61839.502,
|
||||
"min": 1827.42,
|
||||
"max": 124993.694,
|
||||
"ci95": 153630.237
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 74754.411,
|
||||
"stdev": 59189.587,
|
||||
"min": 13600.651,
|
||||
"max": 131761.773,
|
||||
"ci95": 147046.952
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 5571.499,
|
||||
"stdev": 1900.32,
|
||||
"min": 3378.38,
|
||||
"max": 6730.394,
|
||||
"ci95": 4721.037
|
||||
},
|
||||
"chunks.village.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 15287.0,
|
||||
"stdev": 527.501,
|
||||
"min": 14788.0,
|
||||
"max": 15839.0,
|
||||
"ci95": 1310.49
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 2958.557,
|
||||
"stdev": 376.543,
|
||||
"min": 2526.453,
|
||||
"max": 3216.434,
|
||||
"ci95": 935.46
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 368.457,
|
||||
"stdev": 90.668,
|
||||
"min": 300.631,
|
||||
"max": 471.438,
|
||||
"ci95": 225.25
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 91.04,
|
||||
"stdev": 40.506,
|
||||
"min": 46.124,
|
||||
"max": 124.796,
|
||||
"ci95": 100.63
|
||||
},
|
||||
"memory.village.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 581.0,
|
||||
"stdev": 48.0,
|
||||
"min": 533.0,
|
||||
"max": 629.0,
|
||||
"ci95": 119.248
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1277.531,
|
||||
"stdev": 25.546,
|
||||
"min": 1253.879,
|
||||
"max": 1304.623,
|
||||
"ci95": 63.466
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.529,
|
||||
"stdev": 0.127,
|
||||
"min": 0.396,
|
||||
"max": 0.649,
|
||||
"ci95": 0.315
|
||||
},
|
||||
"cpu.village.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.185,
|
||||
"stdev": 0.034,
|
||||
"min": 0.162,
|
||||
"max": 0.224,
|
||||
"ci95": 0.085
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 3318.3,
|
||||
"stdev": 911.608,
|
||||
"min": 2266.627,
|
||||
"max": 3883.075,
|
||||
"ci95": 2264.743
|
||||
},
|
||||
"chunks.forest.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 16565.333,
|
||||
"stdev": 172.755,
|
||||
"min": 16367.0,
|
||||
"max": 16683.0,
|
||||
"ci95": 429.182
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 2333.626,
|
||||
"stdev": 569.124,
|
||||
"min": 1745.365,
|
||||
"max": 2881.457,
|
||||
"ci95": 1413.897
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 336.615,
|
||||
"stdev": 185.666,
|
||||
"min": 208.133,
|
||||
"max": 549.487,
|
||||
"ci95": 461.257
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 160.734,
|
||||
"stdev": 127.586,
|
||||
"min": 39.777,
|
||||
"max": 294.05,
|
||||
"ci95": 316.967
|
||||
},
|
||||
"memory.forest.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 666.333,
|
||||
"stdev": 24.44,
|
||||
"min": 645.0,
|
||||
"max": 693.0,
|
||||
"ci95": 60.718
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1346.816,
|
||||
"stdev": 42.279,
|
||||
"min": 1305.547,
|
||||
"max": 1390.037,
|
||||
"ci95": 105.035
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.669,
|
||||
"stdev": 0.084,
|
||||
"min": 0.598,
|
||||
"max": 0.761,
|
||||
"ci95": 0.208
|
||||
},
|
||||
"cpu.forest.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.222,
|
||||
"stdev": 0.032,
|
||||
"min": 0.197,
|
||||
"max": 0.258,
|
||||
"ci95": 0.079
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 2463.434,
|
||||
"stdev": 1056.0,
|
||||
"min": 1527.516,
|
||||
"max": 3608.283,
|
||||
"ci95": 2623.462
|
||||
},
|
||||
"chunks.ocean.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 14226.667,
|
||||
"stdev": 307.077,
|
||||
"min": 14038.0,
|
||||
"max": 14581.0,
|
||||
"ci95": 762.883
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 2192.874,
|
||||
"stdev": 1320.904,
|
||||
"min": 673.829,
|
||||
"max": 3071.415,
|
||||
"ci95": 3281.573
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 345.406,
|
||||
"stdev": 294.904,
|
||||
"min": 8.977,
|
||||
"max": 559.232,
|
||||
"ci95": 732.642
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 172.573,
|
||||
"stdev": 149.386,
|
||||
"min": 1.038,
|
||||
"max": 274.089,
|
||||
"ci95": 371.125
|
||||
},
|
||||
"memory.ocean.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 629.0,
|
||||
"stdev": 32.0,
|
||||
"min": 597.0,
|
||||
"max": 661.0,
|
||||
"ci95": 79.499
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1352.223,
|
||||
"stdev": 51.951,
|
||||
"min": 1301.492,
|
||||
"max": 1405.314,
|
||||
"ci95": 129.065
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.512,
|
||||
"stdev": 0.099,
|
||||
"min": 0.406,
|
||||
"max": 0.603,
|
||||
"ci95": 0.246
|
||||
},
|
||||
"cpu.ocean.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.178,
|
||||
"stdev": 0.038,
|
||||
"min": 0.135,
|
||||
"max": 0.206,
|
||||
"ci95": 0.094
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 6260.675,
|
||||
"stdev": 3077.348,
|
||||
"min": 2976.342,
|
||||
"max": 9077.55,
|
||||
"ci95": 7645.173
|
||||
},
|
||||
"chunks.mountain.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 16015.333,
|
||||
"stdev": 209.872,
|
||||
"min": 15826.0,
|
||||
"max": 16241.0,
|
||||
"ci95": 521.393
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 1412.095,
|
||||
"stdev": 962.145,
|
||||
"min": 714.773,
|
||||
"max": 2509.775,
|
||||
"ci95": 2390.293
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 182.479,
|
||||
"stdev": 177.563,
|
||||
"min": 13.364,
|
||||
"max": 367.429,
|
||||
"ci95": 441.126
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 88.451,
|
||||
"stdev": 87.578,
|
||||
"min": 1.526,
|
||||
"max": 176.667,
|
||||
"ci95": 217.572
|
||||
},
|
||||
"memory.mountain.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 799.667,
|
||||
"stdev": 18.475,
|
||||
"min": 789.0,
|
||||
"max": 821.0,
|
||||
"ci95": 45.899
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1491.572,
|
||||
"stdev": 36.719,
|
||||
"min": 1459.775,
|
||||
"max": 1531.76,
|
||||
"ci95": 91.221
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.697,
|
||||
"stdev": 0.155,
|
||||
"min": 0.602,
|
||||
"max": 0.876,
|
||||
"ci95": 0.386
|
||||
},
|
||||
"cpu.mountain.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.325,
|
||||
"stdev": 0.115,
|
||||
"min": 0.239,
|
||||
"max": 0.456,
|
||||
"ci95": 0.286
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 1545.896,
|
||||
"stdev": 30.591,
|
||||
"min": 1524.965,
|
||||
"max": 1581.004,
|
||||
"ci95": 75.999
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 1348.389,
|
||||
"stdev": 52.5,
|
||||
"min": 1296.172,
|
||||
"max": 1401.168,
|
||||
"ci95": 130.428
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 37594,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 3132.783,
|
||||
"low_1pct_fps": 471.438,
|
||||
"low_01pct_fps": 124.796,
|
||||
"p50_ms": 0.271,
|
||||
"p95_ms": 0.494,
|
||||
"p99_ms": 1.127,
|
||||
"p999_ms": 2.15,
|
||||
"max_ms": 155.204,
|
||||
"cpu_p50_ms": 0.168,
|
||||
"gpu_p50_ms": 0.104,
|
||||
"histogram": [
|
||||
37151,
|
||||
394,
|
||||
30,
|
||||
8,
|
||||
5,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 38598,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 3216.434,
|
||||
"low_1pct_fps": 300.631,
|
||||
"low_01pct_fps": 46.124,
|
||||
"p50_ms": 0.267,
|
||||
"p95_ms": 0.446,
|
||||
"p99_ms": 1.034,
|
||||
"p999_ms": 1.941,
|
||||
"max_ms": 493.993,
|
||||
"cpu_p50_ms": 0.162,
|
||||
"gpu_p50_ms": 0.104,
|
||||
"histogram": [
|
||||
38177,
|
||||
387,
|
||||
16,
|
||||
8,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 30318,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2526.453,
|
||||
"low_1pct_fps": 333.302,
|
||||
"low_01pct_fps": 102.199,
|
||||
"p50_ms": 0.305,
|
||||
"p95_ms": 0.757,
|
||||
"p99_ms": 1.69,
|
||||
"p999_ms": 3.539,
|
||||
"max_ms": 170.856,
|
||||
"cpu_p50_ms": 0.224,
|
||||
"gpu_p50_ms": 0.106,
|
||||
"histogram": [
|
||||
29368,
|
||||
759,
|
||||
139,
|
||||
35,
|
||||
11,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 34578,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2881.457,
|
||||
"low_1pct_fps": 549.487,
|
||||
"low_01pct_fps": 294.05,
|
||||
"p50_ms": 0.32,
|
||||
"p95_ms": 0.483,
|
||||
"p99_ms": 1.213,
|
||||
"p999_ms": 2.5,
|
||||
"max_ms": 5.358,
|
||||
"cpu_p50_ms": 0.197,
|
||||
"gpu_p50_ms": 0.154,
|
||||
"histogram": [
|
||||
34119,
|
||||
379,
|
||||
64,
|
||||
9,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 28489,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2374.055,
|
||||
"low_1pct_fps": 208.133,
|
||||
"low_01pct_fps": 39.777,
|
||||
"p50_ms": 0.313,
|
||||
"p95_ms": 0.937,
|
||||
"p99_ms": 1.863,
|
||||
"p999_ms": 4.318,
|
||||
"max_ms": 571.48,
|
||||
"cpu_p50_ms": 0.211,
|
||||
"gpu_p50_ms": 0.151,
|
||||
"histogram": [
|
||||
27193,
|
||||
1071,
|
||||
140,
|
||||
51,
|
||||
19,
|
||||
9,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 20945,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1745.365,
|
||||
"low_1pct_fps": 252.226,
|
||||
"low_01pct_fps": 148.375,
|
||||
"p50_ms": 0.343,
|
||||
"p95_ms": 1.505,
|
||||
"p99_ms": 2.758,
|
||||
"p999_ms": 5.341,
|
||||
"max_ms": 11.642,
|
||||
"cpu_p50_ms": 0.258,
|
||||
"gpu_p50_ms": 0.162,
|
||||
"histogram": [
|
||||
18145,
|
||||
2293,
|
||||
337,
|
||||
106,
|
||||
38,
|
||||
13,
|
||||
6,
|
||||
5,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 36856,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 3071.415,
|
||||
"low_1pct_fps": 559.232,
|
||||
"low_01pct_fps": 274.089,
|
||||
"p50_ms": 0.286,
|
||||
"p95_ms": 0.492,
|
||||
"p99_ms": 1.179,
|
||||
"p999_ms": 2.711,
|
||||
"max_ms": 6.718,
|
||||
"cpu_p50_ms": 0.135,
|
||||
"gpu_p50_ms": 0.127,
|
||||
"histogram": [
|
||||
36366,
|
||||
405,
|
||||
60,
|
||||
13,
|
||||
10,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 34001,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2833.377,
|
||||
"low_1pct_fps": 468.008,
|
||||
"low_01pct_fps": 242.591,
|
||||
"p50_ms": 0.295,
|
||||
"p95_ms": 0.589,
|
||||
"p99_ms": 1.41,
|
||||
"p999_ms": 3.048,
|
||||
"max_ms": 7.7,
|
||||
"cpu_p50_ms": 0.206,
|
||||
"gpu_p50_ms": 0.126,
|
||||
"histogram": [
|
||||
33314,
|
||||
544,
|
||||
106,
|
||||
23,
|
||||
10,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 8086,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 673.829,
|
||||
"low_1pct_fps": 8.977,
|
||||
"low_01pct_fps": 1.038,
|
||||
"p50_ms": 0.302,
|
||||
"p95_ms": 0.665,
|
||||
"p99_ms": 2.495,
|
||||
"p999_ms": 15.716,
|
||||
"max_ms": 4849.364,
|
||||
"cpu_p50_ms": 0.194,
|
||||
"gpu_p50_ms": 0.128,
|
||||
"histogram": [
|
||||
7808,
|
||||
166,
|
||||
50,
|
||||
26,
|
||||
8,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
2,
|
||||
5,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 12141,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1011.738,
|
||||
"low_1pct_fps": 166.643,
|
||||
"low_01pct_fps": 87.16,
|
||||
"p50_ms": 0.641,
|
||||
"p95_ms": 2.642,
|
||||
"p99_ms": 4.468,
|
||||
"p999_ms": 7.821,
|
||||
"max_ms": 22.536,
|
||||
"cpu_p50_ms": 0.456,
|
||||
"gpu_p50_ms": 0.138,
|
||||
"histogram": [
|
||||
7840,
|
||||
2996,
|
||||
866,
|
||||
278,
|
||||
87,
|
||||
38,
|
||||
16,
|
||||
11,
|
||||
2,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 30118,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2509.775,
|
||||
"low_1pct_fps": 367.429,
|
||||
"low_01pct_fps": 176.667,
|
||||
"p50_ms": 0.316,
|
||||
"p95_ms": 0.771,
|
||||
"p99_ms": 1.931,
|
||||
"p999_ms": 4.2,
|
||||
"max_ms": 14.633,
|
||||
"cpu_p50_ms": 0.239,
|
||||
"gpu_p50_ms": 0.127,
|
||||
"histogram": [
|
||||
29101,
|
||||
775,
|
||||
173,
|
||||
38,
|
||||
16,
|
||||
8,
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 8577,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 714.773,
|
||||
"low_1pct_fps": 13.364,
|
||||
"low_01pct_fps": 1.526,
|
||||
"p50_ms": 0.416,
|
||||
"p95_ms": 2.257,
|
||||
"p99_ms": 4.36,
|
||||
"p999_ms": 212.217,
|
||||
"max_ms": 1051.428,
|
||||
"cpu_p50_ms": 0.28,
|
||||
"gpu_p50_ms": 0.132,
|
||||
"histogram": [
|
||||
7017,
|
||||
978,
|
||||
377,
|
||||
111,
|
||||
39,
|
||||
24,
|
||||
10,
|
||||
6,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
4,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "horizon-desktop"
|
||||
}
|
||||
@@ -1,870 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T14:47:19Z",
|
||||
"app": {
|
||||
"name": "horizon-client",
|
||||
"version": "nightly-2026-11-04-67dad0d",
|
||||
"commit": "67dad0d"
|
||||
},
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "x86_64",
|
||||
"cpu_threads": 24,
|
||||
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
|
||||
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
|
||||
"ram_mb": 31904,
|
||||
"gpu": {
|
||||
"name": "NVIDIA GeForce RTX 4080",
|
||||
"vendor": "nvidia",
|
||||
"driver": "NVIDIA 595.71.05",
|
||||
"backend": "vulkan",
|
||||
"kind": "discrete"
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 32,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": false,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 29.665,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 45386.16,
|
||||
"stdev": 38970.921,
|
||||
"min": 1077.276,
|
||||
"max": 74343.156,
|
||||
"ci95": 96816.948
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 86585.608,
|
||||
"stdev": 71331.593,
|
||||
"min": 11201.581,
|
||||
"max": 153020.143,
|
||||
"ci95": 177211.803
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 11082.587,
|
||||
"stdev": 5351.291,
|
||||
"min": 7620.454,
|
||||
"max": 17246.096,
|
||||
"ci95": 13294.417
|
||||
},
|
||||
"chunks.village.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 48440.333,
|
||||
"stdev": 656.16,
|
||||
"min": 48060.0,
|
||||
"max": 49198.0,
|
||||
"ci95": 1630.124
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 853.775,
|
||||
"stdev": 345.646,
|
||||
"min": 524.913,
|
||||
"max": 1214.059,
|
||||
"ci95": 858.701
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 130.235,
|
||||
"stdev": 117.39,
|
||||
"min": 9.024,
|
||||
"max": 243.388,
|
||||
"ci95": 291.635
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 70.65,
|
||||
"stdev": 68.628,
|
||||
"min": 1.125,
|
||||
"max": 138.345,
|
||||
"ci95": 170.496
|
||||
},
|
||||
"memory.village.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 2202.333,
|
||||
"stdev": 33.307,
|
||||
"min": 2165.0,
|
||||
"max": 2229.0,
|
||||
"ci95": 82.745
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1652.647,
|
||||
"stdev": 73.452,
|
||||
"min": 1568.063,
|
||||
"max": 1700.344,
|
||||
"ci95": 182.478
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.493,
|
||||
"stdev": 0.169,
|
||||
"min": 0.319,
|
||||
"max": 0.657,
|
||||
"ci95": 0.42
|
||||
},
|
||||
"cpu.village.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.692,
|
||||
"stdev": 0.207,
|
||||
"min": 0.531,
|
||||
"max": 0.925,
|
||||
"ci95": 0.513
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 15019.921,
|
||||
"stdev": 12855.4,
|
||||
"min": 5613.942,
|
||||
"max": 29668.11,
|
||||
"ci95": 31937.162
|
||||
},
|
||||
"chunks.forest.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 80224.667,
|
||||
"stdev": 678.709,
|
||||
"min": 79626.0,
|
||||
"max": 80962.0,
|
||||
"ci95": 1686.142
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 898.208,
|
||||
"stdev": 446.3,
|
||||
"min": 382.944,
|
||||
"max": 1163.61,
|
||||
"ci95": 1108.759
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 178.513,
|
||||
"stdev": 107.096,
|
||||
"min": 54.933,
|
||||
"max": 244.255,
|
||||
"ci95": 266.064
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 103.369,
|
||||
"stdev": 78.403,
|
||||
"min": 12.974,
|
||||
"max": 152.87,
|
||||
"ci95": 194.779
|
||||
},
|
||||
"memory.forest.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 2201.667,
|
||||
"stdev": 34.429,
|
||||
"min": 2163.0,
|
||||
"max": 2229.0,
|
||||
"ci95": 85.532
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1809.063,
|
||||
"stdev": 93.103,
|
||||
"min": 1707.98,
|
||||
"max": 1891.305,
|
||||
"ci95": 231.3
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.585,
|
||||
"stdev": 0.147,
|
||||
"min": 0.493,
|
||||
"max": 0.754,
|
||||
"ci95": 0.365
|
||||
},
|
||||
"cpu.forest.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.989,
|
||||
"stdev": 0.725,
|
||||
"min": 0.57,
|
||||
"max": 1.826,
|
||||
"ci95": 1.802
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 12399.047,
|
||||
"stdev": 15052.073,
|
||||
"min": 3174.115,
|
||||
"max": 29768.486,
|
||||
"ci95": 37394.441
|
||||
},
|
||||
"chunks.ocean.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 64737.0,
|
||||
"stdev": 16149.666,
|
||||
"min": 46089.0,
|
||||
"max": 74089.0,
|
||||
"ci95": 40121.232
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 735.22,
|
||||
"stdev": 601.75,
|
||||
"min": 108.108,
|
||||
"max": 1307.912,
|
||||
"ci95": 1494.952
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 134.654,
|
||||
"stdev": 147.391,
|
||||
"min": 1.252,
|
||||
"max": 292.881,
|
||||
"ci95": 366.169
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 62.062,
|
||||
"stdev": 66.294,
|
||||
"min": 0.202,
|
||||
"max": 132.042,
|
||||
"ci95": 164.697
|
||||
},
|
||||
"memory.ocean.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 1942.333,
|
||||
"stdev": 251.725,
|
||||
"min": 1657.0,
|
||||
"max": 2133.0,
|
||||
"ci95": 625.369
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1812.661,
|
||||
"stdev": 102.065,
|
||||
"min": 1697.912,
|
||||
"max": 1893.313,
|
||||
"ci95": 253.564
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.421,
|
||||
"stdev": 0.227,
|
||||
"min": 0.193,
|
||||
"max": 0.646,
|
||||
"ci95": 0.563
|
||||
},
|
||||
"cpu.ocean.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.513,
|
||||
"stdev": 0.194,
|
||||
"min": 0.303,
|
||||
"max": 0.686,
|
||||
"ci95": 0.483
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 31815.539,
|
||||
"stdev": 36758.245,
|
||||
"min": 9582.166,
|
||||
"max": 74243.988,
|
||||
"ci95": 91319.913
|
||||
},
|
||||
"chunks.mountain.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 90876.333,
|
||||
"stdev": 1525.588,
|
||||
"min": 89793.0,
|
||||
"max": 92621.0,
|
||||
"ci95": 3790.075
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 712.657,
|
||||
"stdev": 252.815,
|
||||
"min": 450.565,
|
||||
"max": 955.041,
|
||||
"ci95": 628.077
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 117.93,
|
||||
"stdev": 85.922,
|
||||
"min": 24.389,
|
||||
"max": 193.34,
|
||||
"ci95": 213.46
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 72.407,
|
||||
"stdev": 65.468,
|
||||
"min": 3.577,
|
||||
"max": 133.895,
|
||||
"ci95": 162.646
|
||||
},
|
||||
"memory.mountain.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 2783.667,
|
||||
"stdev": 48.881,
|
||||
"min": 2741.0,
|
||||
"max": 2837.0,
|
||||
"ci95": 121.436
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1931.359,
|
||||
"stdev": 106.681,
|
||||
"min": 1808.184,
|
||||
"max": 1994.242,
|
||||
"ci95": 265.031
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.03,
|
||||
"stdev": 0.685,
|
||||
"min": 0.564,
|
||||
"max": 1.817,
|
||||
"ci95": 1.702
|
||||
},
|
||||
"cpu.mountain.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.025,
|
||||
"stdev": 0.315,
|
||||
"min": 0.765,
|
||||
"max": 1.376,
|
||||
"ci95": 0.783
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 2051.333,
|
||||
"stdev": 147.151,
|
||||
"min": 1884.176,
|
||||
"max": 2161.316,
|
||||
"ci95": 365.573
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 1810.92,
|
||||
"stdev": 94.623,
|
||||
"min": 1707.342,
|
||||
"max": 1892.828,
|
||||
"ci95": 235.075
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 6301,
|
||||
"seconds": 12.004,
|
||||
"avg_fps": 524.913,
|
||||
"low_1pct_fps": 9.024,
|
||||
"low_01pct_fps": 1.125,
|
||||
"p50_ms": 0.555,
|
||||
"p95_ms": 2.072,
|
||||
"p99_ms": 4.067,
|
||||
"p999_ms": 282.455,
|
||||
"max_ms": 3525.74,
|
||||
"cpu_p50_ms": 0.531,
|
||||
"gpu_p50_ms": 0.218,
|
||||
"histogram": [
|
||||
4878,
|
||||
1060,
|
||||
217,
|
||||
86,
|
||||
19,
|
||||
15,
|
||||
6,
|
||||
2,
|
||||
5,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 9869,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 822.353,
|
||||
"low_1pct_fps": 138.293,
|
||||
"low_01pct_fps": 72.479,
|
||||
"p50_ms": 0.976,
|
||||
"p95_ms": 2.871,
|
||||
"p99_ms": 5.065,
|
||||
"p999_ms": 9.942,
|
||||
"max_ms": 23.337,
|
||||
"cpu_p50_ms": 0.925,
|
||||
"gpu_p50_ms": 0.245,
|
||||
"histogram": [
|
||||
5262,
|
||||
3404,
|
||||
771,
|
||||
248,
|
||||
81,
|
||||
49,
|
||||
23,
|
||||
11,
|
||||
12,
|
||||
3,
|
||||
0,
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 14570,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 1214.059,
|
||||
"low_1pct_fps": 243.388,
|
||||
"low_01pct_fps": 138.345,
|
||||
"p50_ms": 0.655,
|
||||
"p95_ms": 1.486,
|
||||
"p99_ms": 2.883,
|
||||
"p999_ms": 5.366,
|
||||
"max_ms": 26.145,
|
||||
"cpu_p50_ms": 0.62,
|
||||
"gpu_p50_ms": 0.239,
|
||||
"histogram": [
|
||||
11502,
|
||||
2683,
|
||||
257,
|
||||
77,
|
||||
25,
|
||||
23,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 4595,
|
||||
"seconds": 11.999,
|
||||
"avg_fps": 382.944,
|
||||
"low_1pct_fps": 54.933,
|
||||
"low_01pct_fps": 12.974,
|
||||
"p50_ms": 2.269,
|
||||
"p95_ms": 5.193,
|
||||
"p99_ms": 8.719,
|
||||
"p999_ms": 21.933,
|
||||
"max_ms": 243.247,
|
||||
"cpu_p50_ms": 1.826,
|
||||
"gpu_p50_ms": 0.502,
|
||||
"histogram": [
|
||||
273,
|
||||
1654,
|
||||
1395,
|
||||
836,
|
||||
172,
|
||||
122,
|
||||
52,
|
||||
38,
|
||||
26,
|
||||
10,
|
||||
7,
|
||||
5,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 13964,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 1163.61,
|
||||
"low_1pct_fps": 244.255,
|
||||
"low_01pct_fps": 152.87,
|
||||
"p50_ms": 0.682,
|
||||
"p95_ms": 1.524,
|
||||
"p99_ms": 2.877,
|
||||
"p999_ms": 5.801,
|
||||
"max_ms": 7.977,
|
||||
"cpu_p50_ms": 0.57,
|
||||
"gpu_p50_ms": 0.467,
|
||||
"histogram": [
|
||||
11067,
|
||||
2532,
|
||||
235,
|
||||
82,
|
||||
23,
|
||||
16,
|
||||
6,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 13777,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1148.071,
|
||||
"low_1pct_fps": 236.351,
|
||||
"low_01pct_fps": 144.264,
|
||||
"p50_ms": 0.691,
|
||||
"p95_ms": 1.637,
|
||||
"p99_ms": 3.131,
|
||||
"p999_ms": 5.794,
|
||||
"max_ms": 9.301,
|
||||
"cpu_p50_ms": 0.57,
|
||||
"gpu_p50_ms": 0.465,
|
||||
"histogram": [
|
||||
10826,
|
||||
2511,
|
||||
284,
|
||||
106,
|
||||
23,
|
||||
16,
|
||||
5,
|
||||
5,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 1224,
|
||||
"seconds": 11.322,
|
||||
"avg_fps": 108.108,
|
||||
"low_1pct_fps": 1.252,
|
||||
"low_01pct_fps": 0.202,
|
||||
"p50_ms": 0.439,
|
||||
"p95_ms": 3.015,
|
||||
"p99_ms": 7.359,
|
||||
"p999_ms": 2880.23,
|
||||
"max_ms": 7005.436,
|
||||
"cpu_p50_ms": 0.303,
|
||||
"gpu_p50_ms": 0.258,
|
||||
"histogram": [
|
||||
1081,
|
||||
31,
|
||||
49,
|
||||
10,
|
||||
3,
|
||||
7,
|
||||
23,
|
||||
12,
|
||||
4,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 15695,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1307.912,
|
||||
"low_1pct_fps": 292.881,
|
||||
"low_01pct_fps": 132.042,
|
||||
"p50_ms": 0.624,
|
||||
"p95_ms": 1.219,
|
||||
"p99_ms": 2.173,
|
||||
"p999_ms": 4.841,
|
||||
"max_ms": 16.764,
|
||||
"cpu_p50_ms": 0.551,
|
||||
"gpu_p50_ms": 0.401,
|
||||
"histogram": [
|
||||
13558,
|
||||
1932,
|
||||
140,
|
||||
37,
|
||||
14,
|
||||
6,
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 9476,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 789.641,
|
||||
"low_1pct_fps": 109.83,
|
||||
"low_01pct_fps": 53.941,
|
||||
"p50_ms": 0.802,
|
||||
"p95_ms": 3.406,
|
||||
"p99_ms": 5.844,
|
||||
"p999_ms": 14.977,
|
||||
"max_ms": 22.481,
|
||||
"cpu_p50_ms": 0.686,
|
||||
"gpu_p50_ms": 0.403,
|
||||
"histogram": [
|
||||
5819,
|
||||
2151,
|
||||
787,
|
||||
455,
|
||||
112,
|
||||
65,
|
||||
36,
|
||||
18,
|
||||
7,
|
||||
7,
|
||||
7,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 5352,
|
||||
"seconds": 11.878,
|
||||
"avg_fps": 450.565,
|
||||
"low_1pct_fps": 24.389,
|
||||
"low_01pct_fps": 3.577,
|
||||
"p50_ms": 1.431,
|
||||
"p95_ms": 4.548,
|
||||
"p99_ms": 7.601,
|
||||
"p999_ms": 96.086,
|
||||
"max_ms": 714.027,
|
||||
"cpu_p50_ms": 1.376,
|
||||
"gpu_p50_ms": 0.398,
|
||||
"histogram": [
|
||||
1412,
|
||||
2323,
|
||||
811,
|
||||
460,
|
||||
138,
|
||||
84,
|
||||
49,
|
||||
34,
|
||||
13,
|
||||
9,
|
||||
8,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 8789,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 732.365,
|
||||
"low_1pct_fps": 136.06,
|
||||
"low_01pct_fps": 79.749,
|
||||
"p50_ms": 0.972,
|
||||
"p95_ms": 3.329,
|
||||
"p99_ms": 5.68,
|
||||
"p999_ms": 9.7,
|
||||
"max_ms": 18.289,
|
||||
"cpu_p50_ms": 0.935,
|
||||
"gpu_p50_ms": 0.383,
|
||||
"histogram": [
|
||||
4529,
|
||||
3007,
|
||||
715,
|
||||
287,
|
||||
120,
|
||||
61,
|
||||
37,
|
||||
16,
|
||||
12,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 11461,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 955.041,
|
||||
"low_1pct_fps": 193.34,
|
||||
"low_01pct_fps": 133.895,
|
||||
"p50_ms": 0.787,
|
||||
"p95_ms": 2.026,
|
||||
"p99_ms": 3.953,
|
||||
"p999_ms": 6.511,
|
||||
"max_ms": 9.761,
|
||||
"cpu_p50_ms": 0.765,
|
||||
"gpu_p50_ms": 0.379,
|
||||
"histogram": [
|
||||
7754,
|
||||
3114,
|
||||
328,
|
||||
165,
|
||||
46,
|
||||
33,
|
||||
14,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "horizon-desktop"
|
||||
}
|
||||
@@ -1,854 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T12:44:40Z",
|
||||
"app": {
|
||||
"name": "horizon-client",
|
||||
"version": "nightly-2026-11-04-67dad0d",
|
||||
"commit": "67dad0d"
|
||||
},
|
||||
"hardware": {
|
||||
"os": "macos",
|
||||
"arch": "aarch64",
|
||||
"cpu_threads": 14,
|
||||
"os_version": "27.0.1",
|
||||
"cpu": "Apple M4 Pro",
|
||||
"ram_mb": 65536,
|
||||
"gpu": {
|
||||
"name": "Apple M4 Pro",
|
||||
"vendor": "",
|
||||
"driver": "",
|
||||
"backend": "metal",
|
||||
"kind": "integrated"
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 12,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": true,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 17.39,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 1516.155,
|
||||
"stdev": 34.227,
|
||||
"min": 1476.782,
|
||||
"max": 1538.813,
|
||||
"ci95": 85.032
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 4080.9,
|
||||
"stdev": 748.712,
|
||||
"min": 3273.516,
|
||||
"max": 4752.301,
|
||||
"ci95": 1860.054
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 3363.749,
|
||||
"stdev": 224.552,
|
||||
"min": 3105.622,
|
||||
"max": 3514.059,
|
||||
"ci95": 557.864
|
||||
},
|
||||
"chunks.village.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 15473.667,
|
||||
"stdev": 339.602,
|
||||
"min": 15123.0,
|
||||
"max": 15801.0,
|
||||
"ci95": 843.686
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 999.753,
|
||||
"stdev": 41.901,
|
||||
"min": 970.527,
|
||||
"max": 1047.758,
|
||||
"ci95": 104.095
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 336.821,
|
||||
"stdev": 17.834,
|
||||
"min": 318.068,
|
||||
"max": 353.566,
|
||||
"ci95": 44.306
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 263.782,
|
||||
"stdev": 31.556,
|
||||
"min": 227.767,
|
||||
"max": 286.582,
|
||||
"ci95": 78.395
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1147.654,
|
||||
"stdev": 31.82,
|
||||
"min": 1116.875,
|
||||
"max": 1180.422,
|
||||
"ci95": 79.052
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.414,
|
||||
"stdev": 0.036,
|
||||
"min": 0.373,
|
||||
"max": 0.44,
|
||||
"ci95": 0.089
|
||||
},
|
||||
"cpu.village.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.52,
|
||||
"stdev": 0.071,
|
||||
"min": 0.44,
|
||||
"max": 0.577,
|
||||
"ci95": 0.177
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 3685.368,
|
||||
"stdev": 844.43,
|
||||
"min": 2932.117,
|
||||
"max": 4598.204,
|
||||
"ci95": 2097.85
|
||||
},
|
||||
"chunks.forest.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 16795.333,
|
||||
"stdev": 437.802,
|
||||
"min": 16325.0,
|
||||
"max": 17191.0,
|
||||
"ci95": 1087.648
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 592.615,
|
||||
"stdev": 20.339,
|
||||
"min": 580.401,
|
||||
"max": 616.094,
|
||||
"ci95": 50.529
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 250.069,
|
||||
"stdev": 31.227,
|
||||
"min": 217.065,
|
||||
"max": 279.149,
|
||||
"ci95": 77.58
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 151.684,
|
||||
"stdev": 77.064,
|
||||
"min": 64.033,
|
||||
"max": 208.811,
|
||||
"ci95": 191.454
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1064.344,
|
||||
"stdev": 148.185,
|
||||
"min": 901.203,
|
||||
"max": 1190.609,
|
||||
"ci95": 368.142
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.424,
|
||||
"stdev": 0.003,
|
||||
"min": 0.422,
|
||||
"max": 0.427,
|
||||
"ci95": 0.007
|
||||
},
|
||||
"cpu.forest.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.617,
|
||||
"stdev": 0.016,
|
||||
"min": 0.599,
|
||||
"max": 0.629,
|
||||
"ci95": 0.039
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 3594.311,
|
||||
"stdev": 1381.026,
|
||||
"min": 2011.153,
|
||||
"max": 4551.541,
|
||||
"ci95": 3430.936
|
||||
},
|
||||
"chunks.ocean.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 14527.0,
|
||||
"stdev": 319.629,
|
||||
"min": 14166.0,
|
||||
"max": 14774.0,
|
||||
"ci95": 794.068
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 789.003,
|
||||
"stdev": 25.076,
|
||||
"min": 766.83,
|
||||
"max": 816.216,
|
||||
"ci95": 62.297
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 276.887,
|
||||
"stdev": 51.785,
|
||||
"min": 217.373,
|
||||
"max": 311.665,
|
||||
"ci95": 128.651
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 169.278,
|
||||
"stdev": 29.119,
|
||||
"min": 137.156,
|
||||
"max": 193.944,
|
||||
"ci95": 72.341
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 982.995,
|
||||
"stdev": 287.819,
|
||||
"min": 651.109,
|
||||
"max": 1164.063,
|
||||
"ci95": 715.039
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.397,
|
||||
"stdev": 0.028,
|
||||
"min": 0.38,
|
||||
"max": 0.429,
|
||||
"ci95": 0.069
|
||||
},
|
||||
"cpu.ocean.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.569,
|
||||
"stdev": 0.045,
|
||||
"min": 0.533,
|
||||
"max": 0.62,
|
||||
"ci95": 0.112
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 5682.993,
|
||||
"stdev": 1272.063,
|
||||
"min": 4476.873,
|
||||
"max": 7012.069,
|
||||
"ci95": 3160.234
|
||||
},
|
||||
"chunks.mountain.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 15640.0,
|
||||
"stdev": 204.105,
|
||||
"min": 15507.0,
|
||||
"max": 15875.0,
|
||||
"ci95": 507.067
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 716.475,
|
||||
"stdev": 8.139,
|
||||
"min": 707.613,
|
||||
"max": 723.617,
|
||||
"ci95": 20.221
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 313.736,
|
||||
"stdev": 3.759,
|
||||
"min": 311.007,
|
||||
"max": 318.023,
|
||||
"ci95": 9.338
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 270.042,
|
||||
"stdev": 19.572,
|
||||
"min": 247.802,
|
||||
"max": 284.642,
|
||||
"ci95": 48.624
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 997.474,
|
||||
"stdev": 231.733,
|
||||
"min": 790.266,
|
||||
"max": 1247.703,
|
||||
"ci95": 575.704
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.385,
|
||||
"stdev": 0.023,
|
||||
"min": 0.359,
|
||||
"max": 0.401,
|
||||
"ci95": 0.057
|
||||
},
|
||||
"cpu.mountain.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.609,
|
||||
"stdev": 0.044,
|
||||
"min": 0.56,
|
||||
"max": 0.645,
|
||||
"ci95": 0.109
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 1213.255,
|
||||
"stdev": 81.897,
|
||||
"min": 1136.078,
|
||||
"max": 1299.172,
|
||||
"ci95": 203.461
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 1034.279,
|
||||
"stdev": 188.802,
|
||||
"min": 816.453,
|
||||
"max": 1150.961,
|
||||
"ci95": 469.049
|
||||
},
|
||||
"memory.footprint_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 2167.747,
|
||||
"stdev": 14.039,
|
||||
"min": 2153.721,
|
||||
"max": 2181.799,
|
||||
"ci95": 34.878
|
||||
},
|
||||
"memory.footprint_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 1889.468,
|
||||
"stdev": 24.444,
|
||||
"min": 1862.799,
|
||||
"max": 1910.807,
|
||||
"ci95": 60.727
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 12573,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1047.758,
|
||||
"low_1pct_fps": 353.566,
|
||||
"low_01pct_fps": 286.582,
|
||||
"p50_ms": 0.921,
|
||||
"p95_ms": 1.077,
|
||||
"p99_ms": 2.675,
|
||||
"p999_ms": 2.946,
|
||||
"max_ms": 5.36,
|
||||
"cpu_p50_ms": 0.44,
|
||||
"gpu_p50_ms": 0.914,
|
||||
"histogram": [
|
||||
11056,
|
||||
1291,
|
||||
218,
|
||||
6,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 11646,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 970.527,
|
||||
"low_1pct_fps": 318.068,
|
||||
"low_01pct_fps": 227.767,
|
||||
"p50_ms": 0.973,
|
||||
"p95_ms": 1.264,
|
||||
"p99_ms": 2.805,
|
||||
"p999_ms": 3.808,
|
||||
"max_ms": 5.305,
|
||||
"cpu_p50_ms": 0.577,
|
||||
"gpu_p50_ms": 0.969,
|
||||
"histogram": [
|
||||
7035,
|
||||
4334,
|
||||
230,
|
||||
39,
|
||||
6,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 11771,
|
||||
"seconds": 11.999,
|
||||
"avg_fps": 980.973,
|
||||
"low_1pct_fps": 338.828,
|
||||
"low_01pct_fps": 276.996,
|
||||
"p50_ms": 0.985,
|
||||
"p95_ms": 1.157,
|
||||
"p99_ms": 2.779,
|
||||
"p999_ms": 3.068,
|
||||
"max_ms": 4.308,
|
||||
"cpu_p50_ms": 0.542,
|
||||
"gpu_p50_ms": 0.98,
|
||||
"histogram": [
|
||||
6880,
|
||||
4663,
|
||||
208,
|
||||
18,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 7394,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 616.094,
|
||||
"low_1pct_fps": 253.993,
|
||||
"low_01pct_fps": 182.207,
|
||||
"p50_ms": 1.565,
|
||||
"p95_ms": 1.915,
|
||||
"p99_ms": 3.329,
|
||||
"p999_ms": 4.661,
|
||||
"max_ms": 7.32,
|
||||
"cpu_p50_ms": 0.599,
|
||||
"gpu_p50_ms": 1.551,
|
||||
"histogram": [
|
||||
82,
|
||||
6988,
|
||||
144,
|
||||
162,
|
||||
14,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 6965,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 580.401,
|
||||
"low_1pct_fps": 217.065,
|
||||
"low_01pct_fps": 64.033,
|
||||
"p50_ms": 1.654,
|
||||
"p95_ms": 1.983,
|
||||
"p99_ms": 3.233,
|
||||
"p999_ms": 3.893,
|
||||
"max_ms": 81.863,
|
||||
"cpu_p50_ms": 0.629,
|
||||
"gpu_p50_ms": 1.645,
|
||||
"histogram": [
|
||||
11,
|
||||
6622,
|
||||
153,
|
||||
174,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 6977,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 581.35,
|
||||
"low_1pct_fps": 279.149,
|
||||
"low_01pct_fps": 208.811,
|
||||
"p50_ms": 1.659,
|
||||
"p95_ms": 2.006,
|
||||
"p99_ms": 3.273,
|
||||
"p999_ms": 3.982,
|
||||
"max_ms": 5.889,
|
||||
"cpu_p50_ms": 0.623,
|
||||
"gpu_p50_ms": 1.653,
|
||||
"histogram": [
|
||||
17,
|
||||
6606,
|
||||
164,
|
||||
186,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 9793,
|
||||
"seconds": 11.998,
|
||||
"avg_fps": 816.216,
|
||||
"low_1pct_fps": 301.624,
|
||||
"low_01pct_fps": 176.734,
|
||||
"p50_ms": 1.171,
|
||||
"p95_ms": 1.499,
|
||||
"p99_ms": 2.928,
|
||||
"p999_ms": 3.333,
|
||||
"max_ms": 23.472,
|
||||
"cpu_p50_ms": 0.533,
|
||||
"gpu_p50_ms": 1.163,
|
||||
"histogram": [
|
||||
259,
|
||||
9293,
|
||||
182,
|
||||
56,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 9202,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 766.83,
|
||||
"low_1pct_fps": 217.373,
|
||||
"low_01pct_fps": 137.156,
|
||||
"p50_ms": 1.226,
|
||||
"p95_ms": 1.912,
|
||||
"p99_ms": 3.396,
|
||||
"p999_ms": 6.003,
|
||||
"max_ms": 13.562,
|
||||
"cpu_p50_ms": 0.62,
|
||||
"gpu_p50_ms": 1.219,
|
||||
"histogram": [
|
||||
635,
|
||||
8130,
|
||||
261,
|
||||
124,
|
||||
27,
|
||||
15,
|
||||
7,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 9408,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 783.964,
|
||||
"low_1pct_fps": 311.665,
|
||||
"low_01pct_fps": 193.944,
|
||||
"p50_ms": 1.22,
|
||||
"p95_ms": 1.541,
|
||||
"p99_ms": 2.865,
|
||||
"p999_ms": 3.298,
|
||||
"max_ms": 13.861,
|
||||
"cpu_p50_ms": 0.555,
|
||||
"gpu_p50_ms": 1.214,
|
||||
"histogram": [
|
||||
100,
|
||||
9066,
|
||||
204,
|
||||
34,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 8618,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 718.194,
|
||||
"low_1pct_fps": 312.177,
|
||||
"low_01pct_fps": 277.681,
|
||||
"p50_ms": 1.338,
|
||||
"p95_ms": 1.664,
|
||||
"p99_ms": 3.068,
|
||||
"p999_ms": 3.364,
|
||||
"max_ms": 4.008,
|
||||
"cpu_p50_ms": 0.622,
|
||||
"gpu_p50_ms": 1.329,
|
||||
"histogram": [
|
||||
62,
|
||||
8301,
|
||||
130,
|
||||
125,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 8492,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 707.613,
|
||||
"low_1pct_fps": 311.007,
|
||||
"low_01pct_fps": 284.642,
|
||||
"p50_ms": 1.35,
|
||||
"p95_ms": 1.707,
|
||||
"p99_ms": 3.08,
|
||||
"p999_ms": 3.329,
|
||||
"max_ms": 3.796,
|
||||
"cpu_p50_ms": 0.645,
|
||||
"gpu_p50_ms": 1.342,
|
||||
"histogram": [
|
||||
32,
|
||||
8216,
|
||||
111,
|
||||
133,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 8684,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 723.617,
|
||||
"low_1pct_fps": 318.023,
|
||||
"low_01pct_fps": 247.802,
|
||||
"p50_ms": 1.318,
|
||||
"p95_ms": 1.699,
|
||||
"p99_ms": 2.875,
|
||||
"p999_ms": 3.573,
|
||||
"max_ms": 5.186,
|
||||
"cpu_p50_ms": 0.56,
|
||||
"gpu_p50_ms": 1.306,
|
||||
"histogram": [
|
||||
46,
|
||||
8389,
|
||||
199,
|
||||
49,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "horizon-macmini"
|
||||
}
|
||||
@@ -1,854 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T13:17:26Z",
|
||||
"app": {
|
||||
"name": "horizon-client",
|
||||
"version": "nightly-2026-11-04-67dad0d",
|
||||
"commit": "67dad0d"
|
||||
},
|
||||
"hardware": {
|
||||
"os": "macos",
|
||||
"arch": "aarch64",
|
||||
"cpu_threads": 14,
|
||||
"os_version": "27.0.1",
|
||||
"cpu": "Apple M4 Pro",
|
||||
"ram_mb": 65536,
|
||||
"gpu": {
|
||||
"name": "Apple M4 Pro",
|
||||
"vendor": "",
|
||||
"driver": "",
|
||||
"backend": "metal",
|
||||
"kind": "integrated"
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 32,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": true,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 17.712,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 1528.444,
|
||||
"stdev": 181.21,
|
||||
"min": 1319.58,
|
||||
"max": 1643.783,
|
||||
"ci95": 450.188
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 12368.716,
|
||||
"stdev": 6475.37,
|
||||
"min": 7168.429,
|
||||
"max": 19621.62,
|
||||
"ci95": 16087.01
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 6926.208,
|
||||
"stdev": 3712.691,
|
||||
"min": 4208.555,
|
||||
"max": 11156.419,
|
||||
"ci95": 9223.581
|
||||
},
|
||||
"chunks.village.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 46790.333,
|
||||
"stdev": 1381.89,
|
||||
"min": 45236.0,
|
||||
"max": 47880.0,
|
||||
"ci95": 3433.082
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 453.505,
|
||||
"stdev": 14.384,
|
||||
"min": 441.517,
|
||||
"max": 469.455,
|
||||
"ci95": 35.735
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 222.897,
|
||||
"stdev": 10.449,
|
||||
"min": 212.173,
|
||||
"max": 233.048,
|
||||
"ci95": 25.96
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 173.55,
|
||||
"stdev": 31.798,
|
||||
"min": 142.967,
|
||||
"max": 206.437,
|
||||
"ci95": 78.996
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1544.565,
|
||||
"stdev": 26.422,
|
||||
"min": 1516.75,
|
||||
"max": 1569.328,
|
||||
"ci95": 65.64
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.528,
|
||||
"stdev": 0.018,
|
||||
"min": 0.511,
|
||||
"max": 0.548,
|
||||
"ci95": 0.045
|
||||
},
|
||||
"cpu.village.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.496,
|
||||
"stdev": 0.049,
|
||||
"min": 1.453,
|
||||
"max": 1.549,
|
||||
"ci95": 0.121
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 8297.116,
|
||||
"stdev": 689.909,
|
||||
"min": 7716.447,
|
||||
"max": 9059.776,
|
||||
"ci95": 1713.966
|
||||
},
|
||||
"chunks.forest.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 78617.667,
|
||||
"stdev": 494.532,
|
||||
"min": 78228.0,
|
||||
"max": 79174.0,
|
||||
"ci95": 1228.586
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 216.265,
|
||||
"stdev": 6.372,
|
||||
"min": 209.501,
|
||||
"max": 222.156,
|
||||
"ci95": 15.831
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 137.027,
|
||||
"stdev": 3.787,
|
||||
"min": 133.07,
|
||||
"max": 140.617,
|
||||
"ci95": 9.408
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 114.683,
|
||||
"stdev": 7.985,
|
||||
"min": 106.585,
|
||||
"max": 122.551,
|
||||
"ci95": 19.839
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1420.638,
|
||||
"stdev": 247.812,
|
||||
"min": 1266.047,
|
||||
"max": 1706.469,
|
||||
"ci95": 615.649
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.423,
|
||||
"stdev": 0.059,
|
||||
"min": 0.355,
|
||||
"max": 0.458,
|
||||
"ci95": 0.148
|
||||
},
|
||||
"cpu.forest.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.605,
|
||||
"stdev": 0.269,
|
||||
"min": 1.295,
|
||||
"max": 1.779,
|
||||
"ci95": 0.668
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 8354.659,
|
||||
"stdev": 2877.749,
|
||||
"min": 5454.947,
|
||||
"max": 11209.93,
|
||||
"ci95": 7149.301
|
||||
},
|
||||
"chunks.ocean.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 74315.667,
|
||||
"stdev": 63.106,
|
||||
"min": 74246.0,
|
||||
"max": 74369.0,
|
||||
"ci95": 156.776
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 274.828,
|
||||
"stdev": 4.993,
|
||||
"min": 270.041,
|
||||
"max": 280.004,
|
||||
"ci95": 12.404
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 165.765,
|
||||
"stdev": 7.21,
|
||||
"min": 157.538,
|
||||
"max": 170.985,
|
||||
"ci95": 17.912
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 140.185,
|
||||
"stdev": 9.74,
|
||||
"min": 129.976,
|
||||
"max": 149.377,
|
||||
"ci95": 24.198
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1306.578,
|
||||
"stdev": 97.011,
|
||||
"min": 1199.094,
|
||||
"max": 1387.641,
|
||||
"ci95": 241.007
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.416,
|
||||
"stdev": 0.029,
|
||||
"min": 0.392,
|
||||
"max": 0.448,
|
||||
"ci95": 0.071
|
||||
},
|
||||
"cpu.ocean.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.636,
|
||||
"stdev": 0.121,
|
||||
"min": 1.496,
|
||||
"max": 1.708,
|
||||
"ci95": 0.301
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 17124.873,
|
||||
"stdev": 3461.08,
|
||||
"min": 13599.217,
|
||||
"max": 20517.544,
|
||||
"ci95": 8598.494
|
||||
},
|
||||
"chunks.mountain.meshed_count": {
|
||||
"n": 3,
|
||||
"mean": 91909.0,
|
||||
"stdev": 815.603,
|
||||
"min": 91157.0,
|
||||
"max": 92776.0,
|
||||
"ci95": 2026.235
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 242.657,
|
||||
"stdev": 3.405,
|
||||
"min": 240.66,
|
||||
"max": 246.588,
|
||||
"ci95": 8.459
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 110.95,
|
||||
"stdev": 14.622,
|
||||
"min": 99.907,
|
||||
"max": 127.532,
|
||||
"ci95": 36.325
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 51.329,
|
||||
"stdev": 28.253,
|
||||
"min": 30.391,
|
||||
"max": 83.465,
|
||||
"ci95": 70.191
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1451.688,
|
||||
"stdev": 127.471,
|
||||
"min": 1310.969,
|
||||
"max": 1559.43,
|
||||
"ci95": 316.681
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.614,
|
||||
"stdev": 0.017,
|
||||
"min": 0.594,
|
||||
"max": 0.627,
|
||||
"ci95": 0.043
|
||||
},
|
||||
"cpu.mountain.p50_ms": {
|
||||
"n": 3,
|
||||
"mean": 2.233,
|
||||
"stdev": 0.05,
|
||||
"min": 2.194,
|
||||
"max": 2.289,
|
||||
"ci95": 0.124
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 1697.984,
|
||||
"stdev": 29.735,
|
||||
"min": 1667.406,
|
||||
"max": 1726.797,
|
||||
"ci95": 73.871
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 1428.19,
|
||||
"stdev": 120.658,
|
||||
"min": 1333.0,
|
||||
"max": 1563.891,
|
||||
"ci95": 299.756
|
||||
},
|
||||
"memory.footprint_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 4921.753,
|
||||
"stdev": 30.436,
|
||||
"min": 4890.971,
|
||||
"max": 4951.831,
|
||||
"ci95": 75.613
|
||||
},
|
||||
"memory.footprint_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 4079.43,
|
||||
"stdev": 10.978,
|
||||
"min": 4068.049,
|
||||
"max": 4089.956,
|
||||
"ci95": 27.273
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 5634,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 469.455,
|
||||
"low_1pct_fps": 223.469,
|
||||
"low_01pct_fps": 171.246,
|
||||
"p50_ms": 2.054,
|
||||
"p95_ms": 2.492,
|
||||
"p99_ms": 3.993,
|
||||
"p999_ms": 5.425,
|
||||
"max_ms": 6.645,
|
||||
"cpu_p50_ms": 1.453,
|
||||
"gpu_p50_ms": 1.965,
|
||||
"histogram": [
|
||||
0,
|
||||
2082,
|
||||
3324,
|
||||
193,
|
||||
24,
|
||||
9,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 5395,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 449.543,
|
||||
"low_1pct_fps": 233.048,
|
||||
"low_01pct_fps": 206.437,
|
||||
"p50_ms": 2.155,
|
||||
"p95_ms": 2.611,
|
||||
"p99_ms": 4.068,
|
||||
"p999_ms": 4.456,
|
||||
"max_ms": 5.305,
|
||||
"cpu_p50_ms": 1.549,
|
||||
"gpu_p50_ms": 2.043,
|
||||
"histogram": [
|
||||
0,
|
||||
902,
|
||||
4255,
|
||||
206,
|
||||
29,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 5298,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 441.517,
|
||||
"low_1pct_fps": 212.173,
|
||||
"low_01pct_fps": 142.967,
|
||||
"p50_ms": 2.171,
|
||||
"p95_ms": 3.249,
|
||||
"p99_ms": 4.138,
|
||||
"p999_ms": 5.591,
|
||||
"max_ms": 9.676,
|
||||
"cpu_p50_ms": 1.485,
|
||||
"gpu_p50_ms": 2.092,
|
||||
"histogram": [
|
||||
0,
|
||||
709,
|
||||
4314,
|
||||
229,
|
||||
36,
|
||||
6,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 2606,
|
||||
"seconds": 12.002,
|
||||
"avg_fps": 217.137,
|
||||
"low_1pct_fps": 140.617,
|
||||
"low_01pct_fps": 106.585,
|
||||
"p50_ms": 4.471,
|
||||
"p95_ms": 5.926,
|
||||
"p99_ms": 6.575,
|
||||
"p999_ms": 7.735,
|
||||
"max_ms": 12.187,
|
||||
"cpu_p50_ms": 1.74,
|
||||
"gpu_p50_ms": 4.238,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
221,
|
||||
2094,
|
||||
172,
|
||||
110,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 2514,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 209.501,
|
||||
"low_1pct_fps": 137.393,
|
||||
"low_01pct_fps": 122.551,
|
||||
"p50_ms": 4.627,
|
||||
"p95_ms": 6.304,
|
||||
"p99_ms": 6.93,
|
||||
"p999_ms": 7.971,
|
||||
"max_ms": 8.506,
|
||||
"cpu_p50_ms": 1.779,
|
||||
"gpu_p50_ms": 4.409,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
35,
|
||||
2097,
|
||||
200,
|
||||
162,
|
||||
17,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 2667,
|
||||
"seconds": 12.005,
|
||||
"avg_fps": 222.156,
|
||||
"low_1pct_fps": 133.07,
|
||||
"low_01pct_fps": 114.913,
|
||||
"p50_ms": 4.37,
|
||||
"p95_ms": 5.756,
|
||||
"p99_ms": 6.742,
|
||||
"p999_ms": 8.444,
|
||||
"max_ms": 8.984,
|
||||
"cpu_p50_ms": 1.295,
|
||||
"gpu_p50_ms": 4.152,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
440,
|
||||
1978,
|
||||
144,
|
||||
85,
|
||||
16,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 3294,
|
||||
"seconds": 12.003,
|
||||
"avg_fps": 274.44,
|
||||
"low_1pct_fps": 157.538,
|
||||
"low_01pct_fps": 129.976,
|
||||
"p50_ms": 3.521,
|
||||
"p95_ms": 4.814,
|
||||
"p99_ms": 5.592,
|
||||
"p999_ms": 7.491,
|
||||
"max_ms": 7.868,
|
||||
"cpu_p50_ms": 1.703,
|
||||
"gpu_p50_ms": 3.277,
|
||||
"histogram": [
|
||||
0,
|
||||
4,
|
||||
55,
|
||||
2967,
|
||||
130,
|
||||
122,
|
||||
10,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 3241,
|
||||
"seconds": 12.002,
|
||||
"avg_fps": 270.041,
|
||||
"low_1pct_fps": 168.771,
|
||||
"low_01pct_fps": 141.201,
|
||||
"p50_ms": 3.578,
|
||||
"p95_ms": 4.942,
|
||||
"p99_ms": 5.631,
|
||||
"p999_ms": 6.498,
|
||||
"max_ms": 7.601,
|
||||
"cpu_p50_ms": 1.708,
|
||||
"gpu_p50_ms": 3.323,
|
||||
"histogram": [
|
||||
0,
|
||||
2,
|
||||
8,
|
||||
2970,
|
||||
110,
|
||||
144,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 3360,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 280.004,
|
||||
"low_1pct_fps": 170.985,
|
||||
"low_01pct_fps": 149.377,
|
||||
"p50_ms": 3.477,
|
||||
"p95_ms": 4.678,
|
||||
"p99_ms": 5.462,
|
||||
"p999_ms": 6.626,
|
||||
"max_ms": 6.788,
|
||||
"cpu_p50_ms": 1.496,
|
||||
"gpu_p50_ms": 3.23,
|
||||
"histogram": [
|
||||
0,
|
||||
1,
|
||||
5,
|
||||
3125,
|
||||
129,
|
||||
90,
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 2888,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 240.66,
|
||||
"low_1pct_fps": 127.532,
|
||||
"low_01pct_fps": 83.465,
|
||||
"p50_ms": 3.984,
|
||||
"p95_ms": 5.171,
|
||||
"p99_ms": 6.823,
|
||||
"p999_ms": 9.168,
|
||||
"max_ms": 15.341,
|
||||
"cpu_p50_ms": 2.194,
|
||||
"gpu_p50_ms": 3.786,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
2048,
|
||||
638,
|
||||
144,
|
||||
33,
|
||||
16,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 2889,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 240.722,
|
||||
"low_1pct_fps": 99.907,
|
||||
"low_01pct_fps": 40.132,
|
||||
"p50_ms": 3.961,
|
||||
"p95_ms": 5.706,
|
||||
"p99_ms": 7.364,
|
||||
"p999_ms": 12.597,
|
||||
"max_ms": 49.156,
|
||||
"cpu_p50_ms": 2.289,
|
||||
"gpu_p50_ms": 3.802,
|
||||
"histogram": [
|
||||
0,
|
||||
5,
|
||||
126,
|
||||
1859,
|
||||
599,
|
||||
194,
|
||||
72,
|
||||
22,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 2959,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 246.588,
|
||||
"low_1pct_fps": 105.411,
|
||||
"low_01pct_fps": 30.391,
|
||||
"p50_ms": 3.851,
|
||||
"p95_ms": 5.353,
|
||||
"p99_ms": 6.248,
|
||||
"p999_ms": 9.05,
|
||||
"max_ms": 54.378,
|
||||
"cpu_p50_ms": 2.216,
|
||||
"gpu_p50_ms": 3.667,
|
||||
"histogram": [
|
||||
0,
|
||||
2,
|
||||
58,
|
||||
2196,
|
||||
462,
|
||||
196,
|
||||
32,
|
||||
10,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "horizon-macmini"
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# Horizon Nightly 2026-11-04 vs vanilla 26.3 on the Mac mini and the desktop (2026-11-04)
|
||||
|
||||
> **The Mac mini reports (`*-macmini-rd*.json`) are offscreen, not comparable.** Horizon rendered offscreen (no presenting)
|
||||
> while vanilla drew into a window, so the Mac rows below are not a Horizon-vs-vanilla comparison, and the website does not
|
||||
> publish them: it keeps the Mac mini's 2026-10-22 runs. They are kept here only as a record of that day's offscreen run.
|
||||
|
||||
The desktop runs are the ones behind playhorizon.dev/benchmarks as of 2026-11-04 (horizon-rust/horizon-site#58): `tools/perf_bench.py`
|
||||
reports, render distance 12 and 32, three games each, every game from a fresh world (seed 1234), 1280x720, VSync off. Files:
|
||||
`{horizon,vanilla}-macmini-rd{12,32}.json` and `{horizon,vanilla}-desktop-rd{12,32}.json` next to this page.
|
||||
|
||||
## Builds
|
||||
|
||||
- **Horizon:** the Nightly 2026-11-04 release bundle (67dad0d), as the launcher installs it: macOS arm64 on the Mac,
|
||||
Linux x86_64 on the desktop. Its resources come from the bundle's own `horizon-prepare`. perf_bench ran from main
|
||||
a55b9a2e52 and wrote that checkout's crate version and commit; `app.version` and `app.commit` were set to the bundle's by hand.
|
||||
- **Vanilla:** the 26.3 client jar with Fabric Loader and the driver mod, Java 25, the official launcher's JVM flags.
|
||||
|
||||
## Machines and conditions
|
||||
|
||||
- **Mac mini, Apple M4 Pro (14 cores, 20-core GPU), macOS: offscreen, not comparable.** The session was locked, so Horizon ran `--offscreen`: a locked
|
||||
session gives a window no frames, and windowed Horizon measured 0 FPS. Vanilla drew into its window. Offscreen skips
|
||||
presenting, which favours Horizon, so its averages aren't held at the 144 Hz display as they were on 2026-10-22. Each game
|
||||
started at a 1-minute load of 17-18 (the run waited for < 20); during the runs the load rose to 30-50.
|
||||
- **Desktop, RTX 4080 + i7-13700KF, NixOS, NVIDIA 595.** Both games drew into a headless sway rendering on the RTX 4080 (never
|
||||
on its screen), both through Wayland on Vulkan. Vanilla went through SDL's Wayland driver: through Xwayland each of its
|
||||
frames took about 2 s on this day. The machine also runs CI and agents' builds: each game started at a load of 28-30 on
|
||||
24 threads (the run waited for < 30) and the load rose to 40-70 during the runs. Vanilla at RD 32 crashed twice under that
|
||||
load (`GPU timeout attempting to acquire next frame`) before a third series finished. Start-up times are inflated by the
|
||||
load and by a cold disk on each series' first start (start → world up to 130-235 s).
|
||||
- **Order:** RD 12 ran Horizon then vanilla; RD 32 ran vanilla then Horizon.
|
||||
|
||||
## Results (average over the flights of the three games; FPS average / 1% low)
|
||||
|
||||
| Machine | RD | Horizon | Vanilla | Horizon ÷ vanilla (avg, 1% low) |
|
||||
|---|---|---|---|---|
|
||||
| Mac mini (**offscreen, not comparable**) | 12 | 774 / 294 | 132 / 27 | — |
|
||||
| Mac mini (**offscreen, not comparable**) | 32 | 297 / 159 | 68 / 16 | — |
|
||||
| Desktop | 12 | 2,224 / 308 | 1,824 / 201 | 1.2x, 1.5x |
|
||||
| Desktop | 32 | 800 / 140 | 696 / 64 | 1.15x, 2.2x |
|
||||
|
||||
Neither machine was idle, so these say how both games behave on busy machines. They are not clean ratios; for those, see
|
||||
`windows-9800x3d-5080-2026-10-29.md`. The scripts and logs are in horizon-parity `shots/site-bench-2026-11-04/`.
|
||||
@@ -1,787 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T13:28:55Z",
|
||||
"app": {
|
||||
"name": "minecraft",
|
||||
"version": "26.3",
|
||||
"mods": [
|
||||
{
|
||||
"name": "fabric-loader",
|
||||
"version": "0.19.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "x86_64",
|
||||
"cpu_threads": 24,
|
||||
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
|
||||
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
|
||||
"ram_mb": 31904,
|
||||
"gpu": {
|
||||
"name": "NVIDIA GeForce RTX 4080",
|
||||
"vendor": "NVIDIA",
|
||||
"driver": "1.4.329 NVIDIA 595.71.05",
|
||||
"backend": "vulkan",
|
||||
"kind": ""
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 12,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": false,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 28.788,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 121075.467,
|
||||
"stdev": 30646.429,
|
||||
"min": 93272.034,
|
||||
"max": 153935.834,
|
||||
"ci95": 76136.094
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 191063.8,
|
||||
"stdev": 34368.16,
|
||||
"min": 153895.533,
|
||||
"max": 221692.034,
|
||||
"ci95": 85382.134
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 18724.322,
|
||||
"stdev": 9639.048,
|
||||
"min": 13107.549,
|
||||
"max": 29854.376,
|
||||
"ci95": 23946.655
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 2161.532,
|
||||
"stdev": 287.109,
|
||||
"min": 1830.069,
|
||||
"max": 2332.81,
|
||||
"ci95": 713.276
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 162.575,
|
||||
"stdev": 29.799,
|
||||
"min": 136.023,
|
||||
"max": 194.804,
|
||||
"ci95": 74.03
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 27.752,
|
||||
"stdev": 3.186,
|
||||
"min": 24.403,
|
||||
"max": 30.746,
|
||||
"ci95": 7.916
|
||||
},
|
||||
"memory.village.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 13.0,
|
||||
"stdev": 0.0,
|
||||
"min": 13.0,
|
||||
"max": 13.0,
|
||||
"ci95": 0.0
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 2902.054,
|
||||
"stdev": 158.708,
|
||||
"min": 2747.244,
|
||||
"max": 3064.393,
|
||||
"ci95": 394.285
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.691,
|
||||
"stdev": 0.097,
|
||||
"min": 0.601,
|
||||
"max": 0.794,
|
||||
"ci95": 0.241
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 8852.332,
|
||||
"stdev": 530.809,
|
||||
"min": 8350.811,
|
||||
"max": 9408.241,
|
||||
"ci95": 1318.709
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 1841.581,
|
||||
"stdev": 1186.781,
|
||||
"min": 471.209,
|
||||
"max": 2529.847,
|
||||
"ci95": 2948.365
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 255.087,
|
||||
"stdev": 217.128,
|
||||
"min": 5.798,
|
||||
"max": 402.877,
|
||||
"ci95": 539.419
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 92.882,
|
||||
"stdev": 81.722,
|
||||
"min": 0.61,
|
||||
"max": 156.13,
|
||||
"ci95": 203.024
|
||||
},
|
||||
"memory.forest.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 13.0,
|
||||
"stdev": 0.0,
|
||||
"min": 13.0,
|
||||
"max": 13.0,
|
||||
"ci95": 0.0
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 3008.178,
|
||||
"stdev": 149.07,
|
||||
"min": 2875.082,
|
||||
"max": 3169.256,
|
||||
"ci95": 370.34
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.687,
|
||||
"stdev": 0.132,
|
||||
"min": 0.562,
|
||||
"max": 0.825,
|
||||
"ci95": 0.327
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 21215.745,
|
||||
"stdev": 22821.547,
|
||||
"min": 8034.077,
|
||||
"max": 47567.797,
|
||||
"ci95": 56696.441
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 1525.483,
|
||||
"stdev": 1472.651,
|
||||
"min": 633.984,
|
||||
"max": 3225.275,
|
||||
"ci95": 3658.563
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 191.398,
|
||||
"stdev": 272.942,
|
||||
"min": 10.346,
|
||||
"max": 505.335,
|
||||
"ci95": 678.08
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 49.506,
|
||||
"stdev": 66.145,
|
||||
"min": 1.104,
|
||||
"max": 124.874,
|
||||
"ci95": 164.326
|
||||
},
|
||||
"memory.ocean.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 13.0,
|
||||
"stdev": 0.0,
|
||||
"min": 13.0,
|
||||
"max": 13.0,
|
||||
"ci95": 0.0
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 3062.136,
|
||||
"stdev": 128.594,
|
||||
"min": 2962.541,
|
||||
"max": 3207.313,
|
||||
"ci95": 319.472
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.029,
|
||||
"stdev": 0.573,
|
||||
"min": 0.595,
|
||||
"max": 1.678,
|
||||
"ci95": 1.423
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 33255.15,
|
||||
"stdev": 20451.99,
|
||||
"min": 16655.144,
|
||||
"max": 56102.104,
|
||||
"ci95": 50809.661
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 1769.12,
|
||||
"stdev": 996.863,
|
||||
"min": 818.512,
|
||||
"max": 2806.557,
|
||||
"ci95": 2476.544
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 195.916,
|
||||
"stdev": 195.285,
|
||||
"min": 11.586,
|
||||
"max": 400.567,
|
||||
"ci95": 485.154
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 52.202,
|
||||
"stdev": 51.208,
|
||||
"min": 1.461,
|
||||
"max": 103.864,
|
||||
"ci95": 127.217
|
||||
},
|
||||
"memory.mountain.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 13.0,
|
||||
"stdev": 0.0,
|
||||
"min": 13.0,
|
||||
"max": 13.0,
|
||||
"ci95": 0.0
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 2957.948,
|
||||
"stdev": 340.935,
|
||||
"min": 2586.504,
|
||||
"max": 3256.625,
|
||||
"ci95": 846.998
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.436,
|
||||
"stdev": 1.406,
|
||||
"min": 0.462,
|
||||
"max": 3.048,
|
||||
"ci95": 3.494
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 3107.953,
|
||||
"stdev": 141.888,
|
||||
"min": 3020.105,
|
||||
"max": 3271.645,
|
||||
"ci95": 352.498
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 2985.317,
|
||||
"stdev": 210.327,
|
||||
"min": 2761.164,
|
||||
"max": 3178.35,
|
||||
"ci95": 522.523
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 21963,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 1830.069,
|
||||
"low_1pct_fps": 136.023,
|
||||
"low_01pct_fps": 30.746,
|
||||
"p50_ms": 0.331,
|
||||
"p95_ms": 1.153,
|
||||
"p99_ms": 3.001,
|
||||
"p999_ms": 9.997,
|
||||
"max_ms": 326.36,
|
||||
"histogram": [
|
||||
20336,
|
||||
1238,
|
||||
167,
|
||||
108,
|
||||
40,
|
||||
23,
|
||||
12,
|
||||
10,
|
||||
8,
|
||||
7,
|
||||
5,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 27994,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2332.81,
|
||||
"low_1pct_fps": 194.804,
|
||||
"low_01pct_fps": 28.107,
|
||||
"p50_ms": 0.321,
|
||||
"p95_ms": 0.771,
|
||||
"p99_ms": 1.356,
|
||||
"p999_ms": 3.836,
|
||||
"max_ms": 331.931,
|
||||
"histogram": [
|
||||
27270,
|
||||
652,
|
||||
34,
|
||||
13,
|
||||
5,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 27861,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2321.717,
|
||||
"low_1pct_fps": 156.898,
|
||||
"low_01pct_fps": 24.403,
|
||||
"p50_ms": 0.281,
|
||||
"p95_ms": 0.852,
|
||||
"p99_ms": 1.73,
|
||||
"p999_ms": 6.331,
|
||||
"max_ms": 301.991,
|
||||
"histogram": [
|
||||
26932,
|
||||
726,
|
||||
124,
|
||||
42,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
9,
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 30359,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2529.847,
|
||||
"low_1pct_fps": 356.586,
|
||||
"low_01pct_fps": 121.906,
|
||||
"p50_ms": 0.353,
|
||||
"p95_ms": 0.563,
|
||||
"p99_ms": 1.405,
|
||||
"p999_ms": 4.444,
|
||||
"max_ms": 20.302,
|
||||
"histogram": [
|
||||
29756,
|
||||
460,
|
||||
57,
|
||||
52,
|
||||
11,
|
||||
5,
|
||||
3,
|
||||
5,
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 5781,
|
||||
"seconds": 12.268,
|
||||
"avg_fps": 471.209,
|
||||
"low_1pct_fps": 5.798,
|
||||
"low_01pct_fps": 0.61,
|
||||
"p50_ms": 0.359,
|
||||
"p95_ms": 0.77,
|
||||
"p99_ms": 1.78,
|
||||
"p999_ms": 20.826,
|
||||
"max_ms": 5834.37,
|
||||
"histogram": [
|
||||
5586,
|
||||
158,
|
||||
12,
|
||||
11,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 30285,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2523.687,
|
||||
"low_1pct_fps": 402.877,
|
||||
"low_01pct_fps": 156.13,
|
||||
"p50_ms": 0.351,
|
||||
"p95_ms": 0.539,
|
||||
"p99_ms": 1.574,
|
||||
"p999_ms": 3.683,
|
||||
"max_ms": 16.022,
|
||||
"histogram": [
|
||||
29666,
|
||||
490,
|
||||
74,
|
||||
35,
|
||||
7,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
5,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 11685,
|
||||
"seconds": 16.293,
|
||||
"avg_fps": 717.19,
|
||||
"low_1pct_fps": 10.346,
|
||||
"low_01pct_fps": 1.104,
|
||||
"p50_ms": 0.299,
|
||||
"p95_ms": 1.119,
|
||||
"p99_ms": 2.568,
|
||||
"p999_ms": 12.054,
|
||||
"max_ms": 5250.537,
|
||||
"histogram": [
|
||||
10993,
|
||||
502,
|
||||
102,
|
||||
39,
|
||||
14,
|
||||
8,
|
||||
6,
|
||||
4,
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 38704,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 3225.275,
|
||||
"low_1pct_fps": 505.335,
|
||||
"low_01pct_fps": 124.874,
|
||||
"p50_ms": 0.283,
|
||||
"p95_ms": 0.36,
|
||||
"p99_ms": 0.909,
|
||||
"p999_ms": 2.015,
|
||||
"max_ms": 132.801,
|
||||
"histogram": [
|
||||
38385,
|
||||
279,
|
||||
25,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 7609,
|
||||
"seconds": 12.002,
|
||||
"avg_fps": 633.984,
|
||||
"low_1pct_fps": 58.513,
|
||||
"low_01pct_fps": 22.539,
|
||||
"p50_ms": 1.141,
|
||||
"p95_ms": 4.438,
|
||||
"p99_ms": 9.113,
|
||||
"p999_ms": 27.496,
|
||||
"max_ms": 64.424,
|
||||
"histogram": [
|
||||
3452,
|
||||
2398,
|
||||
956,
|
||||
368,
|
||||
143,
|
||||
96,
|
||||
61,
|
||||
46,
|
||||
20,
|
||||
29,
|
||||
6,
|
||||
10,
|
||||
6,
|
||||
7,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 10168,
|
||||
"seconds": 12.423,
|
||||
"avg_fps": 818.512,
|
||||
"low_1pct_fps": 11.586,
|
||||
"low_01pct_fps": 1.461,
|
||||
"p50_ms": 0.267,
|
||||
"p95_ms": 0.892,
|
||||
"p99_ms": 3.176,
|
||||
"p999_ms": 198.757,
|
||||
"max_ms": 1882.64,
|
||||
"histogram": [
|
||||
9733,
|
||||
205,
|
||||
121,
|
||||
49,
|
||||
12,
|
||||
9,
|
||||
8,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
5,
|
||||
1,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 33679,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 2806.557,
|
||||
"low_1pct_fps": 400.567,
|
||||
"low_01pct_fps": 103.864,
|
||||
"p50_ms": 0.324,
|
||||
"p95_ms": 0.411,
|
||||
"p99_ms": 1.167,
|
||||
"p999_ms": 3.508,
|
||||
"max_ms": 83.572,
|
||||
"histogram": [
|
||||
33272,
|
||||
315,
|
||||
46,
|
||||
22,
|
||||
2,
|
||||
1,
|
||||
4,
|
||||
2,
|
||||
9,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 20188,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1682.292,
|
||||
"low_1pct_fps": 175.594,
|
||||
"low_01pct_fps": 51.28,
|
||||
"p50_ms": 0.374,
|
||||
"p95_ms": 1.37,
|
||||
"p99_ms": 2.821,
|
||||
"p999_ms": 8.947,
|
||||
"max_ms": 53.494,
|
||||
"histogram": [
|
||||
18109,
|
||||
1592,
|
||||
314,
|
||||
83,
|
||||
36,
|
||||
19,
|
||||
7,
|
||||
6,
|
||||
6,
|
||||
1,
|
||||
3,
|
||||
1,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "vanilla-desktop"
|
||||
}
|
||||
@@ -1,787 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T15:46:25Z",
|
||||
"app": {
|
||||
"name": "minecraft",
|
||||
"version": "26.3",
|
||||
"mods": [
|
||||
{
|
||||
"name": "fabric-loader",
|
||||
"version": "0.19.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "x86_64",
|
||||
"cpu_threads": 24,
|
||||
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
|
||||
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
|
||||
"ram_mb": 31904,
|
||||
"gpu": {
|
||||
"name": "NVIDIA GeForce RTX 4080",
|
||||
"vendor": "NVIDIA",
|
||||
"driver": "1.4.329 NVIDIA 595.71.05",
|
||||
"backend": "vulkan",
|
||||
"kind": ""
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 32,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": false,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 28.799,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 112742.179,
|
||||
"stdev": 27885.804,
|
||||
"min": 81110.692,
|
||||
"max": 133773.748,
|
||||
"ci95": 69277.769
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 235049.179,
|
||||
"stdev": 31461.958,
|
||||
"min": 203486.748,
|
||||
"max": 266409.692,
|
||||
"ci95": 78162.144
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 79828.05,
|
||||
"stdev": 109107.365,
|
||||
"min": 16685.972,
|
||||
"max": 205814.266,
|
||||
"ci95": 271059.597
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 525.667,
|
||||
"stdev": 390.505,
|
||||
"min": 101.073,
|
||||
"max": 869.433,
|
||||
"ci95": 970.146
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 44.209,
|
||||
"stdev": 39.491,
|
||||
"min": 1.644,
|
||||
"max": 79.658,
|
||||
"ci95": 98.109
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 10.635,
|
||||
"stdev": 9.759,
|
||||
"min": 0.686,
|
||||
"max": 20.192,
|
||||
"ci95": 24.244
|
||||
},
|
||||
"memory.village.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 34.333,
|
||||
"stdev": 36.95,
|
||||
"min": 13.0,
|
||||
"max": 77.0,
|
||||
"ci95": 91.797
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 3296.229,
|
||||
"stdev": 296.464,
|
||||
"min": 2956.234,
|
||||
"max": 3500.773,
|
||||
"ci95": 736.517
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.23,
|
||||
"stdev": 0.449,
|
||||
"min": 0.897,
|
||||
"max": 1.74,
|
||||
"ci95": 1.114
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 61172.527,
|
||||
"stdev": 17434.762,
|
||||
"min": 46779.931,
|
||||
"max": 80559.429,
|
||||
"ci95": 43313.846
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 599.408,
|
||||
"stdev": 492.96,
|
||||
"min": 64.173,
|
||||
"max": 1034.813,
|
||||
"ci95": 1224.68
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 61.923,
|
||||
"stdev": 56.534,
|
||||
"min": 0.737,
|
||||
"max": 112.221,
|
||||
"ci95": 140.449
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 15.771,
|
||||
"stdev": 14.11,
|
||||
"min": 0.098,
|
||||
"max": 27.463,
|
||||
"ci95": 35.054
|
||||
},
|
||||
"memory.forest.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 34.333,
|
||||
"stdev": 36.95,
|
||||
"min": 13.0,
|
||||
"max": 77.0,
|
||||
"ci95": 91.797
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 3278.964,
|
||||
"stdev": 301.465,
|
||||
"min": 2947.268,
|
||||
"max": 3536.27,
|
||||
"ci95": 748.941
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 5.908,
|
||||
"stdev": 8.813,
|
||||
"min": 0.693,
|
||||
"max": 16.084,
|
||||
"ci95": 21.895
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 61762.098,
|
||||
"stdev": 3569.17,
|
||||
"min": 57919.097,
|
||||
"max": 64972.973,
|
||||
"ci95": 8867.026
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 1175.044,
|
||||
"stdev": 131.94,
|
||||
"min": 1093.801,
|
||||
"max": 1327.281,
|
||||
"ci95": 327.785
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 119.758,
|
||||
"stdev": 68.419,
|
||||
"min": 77.252,
|
||||
"max": 198.683,
|
||||
"ci95": 169.976
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 24.224,
|
||||
"stdev": 18.982,
|
||||
"min": 12.308,
|
||||
"max": 46.114,
|
||||
"ci95": 47.159
|
||||
},
|
||||
"memory.ocean.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 34.333,
|
||||
"stdev": 36.95,
|
||||
"min": 13.0,
|
||||
"max": 77.0,
|
||||
"ci95": 91.797
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 3275.349,
|
||||
"stdev": 317.864,
|
||||
"min": 2924.113,
|
||||
"max": 3543.23,
|
||||
"ci95": 789.681
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.654,
|
||||
"stdev": 0.066,
|
||||
"min": 0.579,
|
||||
"max": 0.699,
|
||||
"ci95": 0.163
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 113439.529,
|
||||
"stdev": 64746.162,
|
||||
"min": 55046.887,
|
||||
"max": 183068.386,
|
||||
"ci95": 160851.363
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 485.832,
|
||||
"stdev": 317.254,
|
||||
"min": 121.312,
|
||||
"max": 699.617,
|
||||
"ci95": 788.165
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 28.612,
|
||||
"stdev": 23.334,
|
||||
"min": 2.229,
|
||||
"max": 46.542,
|
||||
"ci95": 57.97
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 4.56,
|
||||
"stdev": 3.408,
|
||||
"min": 0.75,
|
||||
"max": 7.318,
|
||||
"ci95": 8.467
|
||||
},
|
||||
"memory.mountain.gpu_mb": {
|
||||
"n": 3,
|
||||
"mean": 34.333,
|
||||
"stdev": 36.95,
|
||||
"min": 13.0,
|
||||
"max": 77.0,
|
||||
"ci95": 91.797
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 3248.708,
|
||||
"stdev": 480.962,
|
||||
"min": 2700.645,
|
||||
"max": 3600.484,
|
||||
"ci95": 1194.873
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.832,
|
||||
"stdev": 2.076,
|
||||
"min": 0.618,
|
||||
"max": 4.229,
|
||||
"ci95": 5.157
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 3371.958,
|
||||
"stdev": 280.347,
|
||||
"min": 3059.926,
|
||||
"max": 3602.613,
|
||||
"ci95": 696.476
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 3291.249,
|
||||
"stdev": 316.441,
|
||||
"min": 2935.975,
|
||||
"max": 3542.848,
|
||||
"ci95": 786.147
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 1248,
|
||||
"seconds": 12.348,
|
||||
"avg_fps": 101.073,
|
||||
"low_1pct_fps": 1.644,
|
||||
"low_01pct_fps": 0.686,
|
||||
"p50_ms": 1.225,
|
||||
"p95_ms": 11.161,
|
||||
"p99_ms": 226.597,
|
||||
"p999_ms": 1457.605,
|
||||
"max_ms": 1457.874,
|
||||
"histogram": [
|
||||
548,
|
||||
232,
|
||||
97,
|
||||
118,
|
||||
51,
|
||||
49,
|
||||
24,
|
||||
39,
|
||||
16,
|
||||
17,
|
||||
13,
|
||||
6,
|
||||
3,
|
||||
2,
|
||||
5,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
8,
|
||||
1,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 10435,
|
||||
"seconds": 12.002,
|
||||
"avg_fps": 869.433,
|
||||
"low_1pct_fps": 79.658,
|
||||
"low_01pct_fps": 20.192,
|
||||
"p50_ms": 0.813,
|
||||
"p95_ms": 2.537,
|
||||
"p99_ms": 5.685,
|
||||
"p999_ms": 13.292,
|
||||
"max_ms": 351.251,
|
||||
"histogram": [
|
||||
7598,
|
||||
2071,
|
||||
393,
|
||||
171,
|
||||
55,
|
||||
53,
|
||||
22,
|
||||
24,
|
||||
20,
|
||||
10,
|
||||
9,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 7278,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 606.495,
|
||||
"low_1pct_fps": 51.326,
|
||||
"low_01pct_fps": 11.026,
|
||||
"p50_ms": 1.105,
|
||||
"p95_ms": 3.854,
|
||||
"p99_ms": 7.067,
|
||||
"p999_ms": 24.484,
|
||||
"max_ms": 443.95,
|
||||
"histogram": [
|
||||
3206,
|
||||
2621,
|
||||
800,
|
||||
360,
|
||||
112,
|
||||
71,
|
||||
34,
|
||||
26,
|
||||
12,
|
||||
13,
|
||||
3,
|
||||
5,
|
||||
2,
|
||||
7,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 975,
|
||||
"seconds": 15.193,
|
||||
"avg_fps": 64.173,
|
||||
"low_1pct_fps": 0.737,
|
||||
"low_01pct_fps": 0.098,
|
||||
"p50_ms": 0.864,
|
||||
"p95_ms": 5.083,
|
||||
"p99_ms": 77.689,
|
||||
"p999_ms": 10225.237,
|
||||
"max_ms": 10225.237,
|
||||
"histogram": [
|
||||
680,
|
||||
99,
|
||||
104,
|
||||
12,
|
||||
30,
|
||||
13,
|
||||
8,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 8391,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 699.237,
|
||||
"low_1pct_fps": 72.811,
|
||||
"low_01pct_fps": 19.752,
|
||||
"p50_ms": 1.087,
|
||||
"p95_ms": 2.927,
|
||||
"p99_ms": 5.909,
|
||||
"p999_ms": 15.268,
|
||||
"max_ms": 252.557,
|
||||
"histogram": [
|
||||
3342,
|
||||
3998,
|
||||
656,
|
||||
231,
|
||||
56,
|
||||
26,
|
||||
15,
|
||||
19,
|
||||
12,
|
||||
12,
|
||||
11,
|
||||
6,
|
||||
1,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 12418,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1034.813,
|
||||
"low_1pct_fps": 112.221,
|
||||
"low_01pct_fps": 27.463,
|
||||
"p50_ms": 0.837,
|
||||
"p95_ms": 1.394,
|
||||
"p99_ms": 3.491,
|
||||
"p999_ms": 13.217,
|
||||
"max_ms": 245.59,
|
||||
"histogram": [
|
||||
11155,
|
||||
1004,
|
||||
100,
|
||||
64,
|
||||
29,
|
||||
16,
|
||||
16,
|
||||
10,
|
||||
2,
|
||||
6,
|
||||
5,
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 13126,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1093.801,
|
||||
"low_1pct_fps": 77.252,
|
||||
"low_01pct_fps": 12.308,
|
||||
"p50_ms": 0.714,
|
||||
"p95_ms": 1.402,
|
||||
"p99_ms": 2.875,
|
||||
"p999_ms": 12.067,
|
||||
"max_ms": 238.158,
|
||||
"histogram": [
|
||||
11605,
|
||||
1207,
|
||||
191,
|
||||
54,
|
||||
17,
|
||||
11,
|
||||
7,
|
||||
12,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 15928,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1327.281,
|
||||
"low_1pct_fps": 198.683,
|
||||
"low_01pct_fps": 46.114,
|
||||
"p50_ms": 0.67,
|
||||
"p95_ms": 1.172,
|
||||
"p99_ms": 2.111,
|
||||
"p999_ms": 7.067,
|
||||
"max_ms": 193.743,
|
||||
"histogram": [
|
||||
14891,
|
||||
856,
|
||||
102,
|
||||
42,
|
||||
7,
|
||||
6,
|
||||
8,
|
||||
6,
|
||||
0,
|
||||
5,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 13249,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 1104.051,
|
||||
"low_1pct_fps": 83.338,
|
||||
"low_01pct_fps": 14.249,
|
||||
"p50_ms": 0.711,
|
||||
"p95_ms": 1.403,
|
||||
"p99_ms": 3.046,
|
||||
"p999_ms": 10.625,
|
||||
"max_ms": 451.869,
|
||||
"histogram": [
|
||||
11824,
|
||||
1165,
|
||||
124,
|
||||
58,
|
||||
13,
|
||||
16,
|
||||
11,
|
||||
14,
|
||||
7,
|
||||
7,
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 1469,
|
||||
"seconds": 12.109,
|
||||
"avg_fps": 121.312,
|
||||
"low_1pct_fps": 2.229,
|
||||
"low_01pct_fps": 0.75,
|
||||
"p50_ms": 2.12,
|
||||
"p95_ms": 13.173,
|
||||
"p99_ms": 125.189,
|
||||
"p999_ms": 727.045,
|
||||
"max_ms": 1939.723,
|
||||
"histogram": [
|
||||
196,
|
||||
440,
|
||||
438,
|
||||
136,
|
||||
50,
|
||||
36,
|
||||
24,
|
||||
25,
|
||||
22,
|
||||
17,
|
||||
17,
|
||||
13,
|
||||
6,
|
||||
11,
|
||||
8,
|
||||
4,
|
||||
1,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
7,
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 8396,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 699.617,
|
||||
"low_1pct_fps": 46.542,
|
||||
"low_01pct_fps": 7.318,
|
||||
"p50_ms": 1.022,
|
||||
"p95_ms": 2.433,
|
||||
"p99_ms": 5.528,
|
||||
"p999_ms": 13.993,
|
||||
"max_ms": 305.464,
|
||||
"histogram": [
|
||||
3248,
|
||||
4509,
|
||||
361,
|
||||
113,
|
||||
52,
|
||||
37,
|
||||
21,
|
||||
26,
|
||||
14,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 7639,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 636.566,
|
||||
"low_1pct_fps": 37.064,
|
||||
"low_01pct_fps": 5.612,
|
||||
"p50_ms": 1.023,
|
||||
"p95_ms": 3.0,
|
||||
"p99_ms": 7.11,
|
||||
"p999_ms": 17.521,
|
||||
"max_ms": 268.877,
|
||||
"histogram": [
|
||||
3051,
|
||||
3794,
|
||||
413,
|
||||
158,
|
||||
66,
|
||||
43,
|
||||
35,
|
||||
21,
|
||||
31,
|
||||
12,
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
2,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "vanilla-desktop"
|
||||
}
|
||||
@@ -1,771 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T12:54:29Z",
|
||||
"app": {
|
||||
"name": "minecraft",
|
||||
"version": "26.3",
|
||||
"mods": [
|
||||
{
|
||||
"name": "fabric-loader",
|
||||
"version": "0.19.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hardware": {
|
||||
"os": "macos",
|
||||
"arch": "aarch64",
|
||||
"cpu_threads": 14,
|
||||
"os_version": "27.0.1",
|
||||
"cpu": "Apple M4 Pro",
|
||||
"ram_mb": 65536,
|
||||
"gpu": {
|
||||
"name": "Apple M4 Pro",
|
||||
"vendor": "Apple",
|
||||
"driver": "4.1 Metal - 91.7",
|
||||
"backend": "opengl",
|
||||
"kind": ""
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 12,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": false,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 17.628,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 12652.182,
|
||||
"stdev": 3231.342,
|
||||
"min": 9526.004,
|
||||
"max": 15979.309,
|
||||
"ci95": 8027.746
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 31329.516,
|
||||
"stdev": 10016.129,
|
||||
"min": 20777.004,
|
||||
"max": 40705.309,
|
||||
"ci95": 24883.451
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 12623.084,
|
||||
"stdev": 4958.724,
|
||||
"min": 7251.986,
|
||||
"max": 17026.894,
|
||||
"ci95": 12319.147
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 92.729,
|
||||
"stdev": 58.527,
|
||||
"min": 28.713,
|
||||
"max": 143.496,
|
||||
"ci95": 145.402
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 16.52,
|
||||
"stdev": 10.949,
|
||||
"min": 4.001,
|
||||
"max": 24.308,
|
||||
"ci95": 27.2
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 11.515,
|
||||
"stdev": 7.155,
|
||||
"min": 3.549,
|
||||
"max": 17.394,
|
||||
"ci95": 17.774
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1834.469,
|
||||
"stdev": 329.198,
|
||||
"min": 1455.688,
|
||||
"max": 2051.516,
|
||||
"ci95": 817.839
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.903,
|
||||
"stdev": 1.114,
|
||||
"min": 1.131,
|
||||
"max": 3.18,
|
||||
"ci95": 2.767
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 14590.694,
|
||||
"stdev": 3260.615,
|
||||
"min": 10942.613,
|
||||
"max": 17221.108,
|
||||
"ci95": 8100.47
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 106.817,
|
||||
"stdev": 104.558,
|
||||
"min": 43.857,
|
||||
"max": 227.513,
|
||||
"ci95": 259.759
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 23.0,
|
||||
"stdev": 26.394,
|
||||
"min": 7.338,
|
||||
"max": 53.473,
|
||||
"ci95": 65.57
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 14.841,
|
||||
"stdev": 15.773,
|
||||
"min": 5.157,
|
||||
"max": 33.042,
|
||||
"ci95": 39.186
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1814.245,
|
||||
"stdev": 154.974,
|
||||
"min": 1635.453,
|
||||
"max": 1910.125,
|
||||
"ci95": 385.007
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.452,
|
||||
"stdev": 0.542,
|
||||
"min": 0.826,
|
||||
"max": 1.777,
|
||||
"ci95": 1.346
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 13309.049,
|
||||
"stdev": 8088.157,
|
||||
"min": 6394.403,
|
||||
"max": 22203.194,
|
||||
"ci95": 20093.716
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 164.405,
|
||||
"stdev": 141.4,
|
||||
"min": 62.162,
|
||||
"max": 325.77,
|
||||
"ci95": 351.285
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 36.242,
|
||||
"stdev": 37.051,
|
||||
"min": 14.736,
|
||||
"max": 79.024,
|
||||
"ci95": 92.047
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 20.898,
|
||||
"stdev": 17.899,
|
||||
"min": 8.855,
|
||||
"max": 41.466,
|
||||
"ci95": 44.468
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1724.453,
|
||||
"stdev": 347.692,
|
||||
"min": 1323.5,
|
||||
"max": 1942.734,
|
||||
"ci95": 863.784
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.866,
|
||||
"stdev": 0.248,
|
||||
"min": 0.58,
|
||||
"max": 1.03,
|
||||
"ci95": 0.617
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 17989.43,
|
||||
"stdev": 3969.295,
|
||||
"min": 13444.423,
|
||||
"max": 20774.261,
|
||||
"ci95": 9861.07
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 162.084,
|
||||
"stdev": 160.057,
|
||||
"min": 68.44,
|
||||
"max": 346.896,
|
||||
"ci95": 397.635
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 33.74,
|
||||
"stdev": 36.146,
|
||||
"min": 11.158,
|
||||
"max": 75.429,
|
||||
"ci95": 89.798
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 17.194,
|
||||
"stdev": 14.232,
|
||||
"min": 7.903,
|
||||
"max": 33.579,
|
||||
"ci95": 35.357
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 1977.602,
|
||||
"stdev": 108.627,
|
||||
"min": 1872.516,
|
||||
"max": 2089.453,
|
||||
"ci95": 269.866
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.772,
|
||||
"stdev": 0.332,
|
||||
"min": 0.463,
|
||||
"max": 1.123,
|
||||
"ci95": 0.825
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 2135.313,
|
||||
"stdev": 54.617,
|
||||
"min": 2072.344,
|
||||
"max": 2169.828,
|
||||
"ci95": 135.686
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 1836.44,
|
||||
"stdev": 223.694,
|
||||
"min": 1578.141,
|
||||
"max": 1965.781,
|
||||
"ci95": 555.732
|
||||
},
|
||||
"memory.footprint_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 3253.456,
|
||||
"stdev": 68.323,
|
||||
"min": 3178.409,
|
||||
"max": 3312.05,
|
||||
"ci95": 169.737
|
||||
},
|
||||
"memory.footprint_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 3144.363,
|
||||
"stdev": 69.868,
|
||||
"min": 3069.394,
|
||||
"max": 3207.66,
|
||||
"ci95": 173.576
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 1272,
|
||||
"seconds": 12.002,
|
||||
"avg_fps": 105.979,
|
||||
"low_1pct_fps": 21.25,
|
||||
"low_01pct_fps": 17.394,
|
||||
"p50_ms": 7.527,
|
||||
"p95_ms": 22.34,
|
||||
"p99_ms": 40.461,
|
||||
"p999_ms": 54.28,
|
||||
"max_ms": 60.7,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
56,
|
||||
161,
|
||||
170,
|
||||
180,
|
||||
167,
|
||||
184,
|
||||
118,
|
||||
71,
|
||||
61,
|
||||
26,
|
||||
25,
|
||||
28,
|
||||
12,
|
||||
10,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 1723,
|
||||
"seconds": 12.007,
|
||||
"avg_fps": 143.496,
|
||||
"low_1pct_fps": 24.308,
|
||||
"low_01pct_fps": 13.602,
|
||||
"p50_ms": 5.317,
|
||||
"p95_ms": 16.181,
|
||||
"p99_ms": 29.771,
|
||||
"p999_ms": 56.832,
|
||||
"max_ms": 90.209,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
63,
|
||||
413,
|
||||
271,
|
||||
273,
|
||||
185,
|
||||
148,
|
||||
112,
|
||||
89,
|
||||
47,
|
||||
43,
|
||||
28,
|
||||
22,
|
||||
18,
|
||||
4,
|
||||
5,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 346,
|
||||
"seconds": 12.05,
|
||||
"avg_fps": 28.713,
|
||||
"low_1pct_fps": 4.001,
|
||||
"low_01pct_fps": 3.549,
|
||||
"p50_ms": 23.684,
|
||||
"p95_ms": 110.057,
|
||||
"p99_ms": 225.236,
|
||||
"p999_ms": 281.776,
|
||||
"max_ms": 281.776,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
8,
|
||||
6,
|
||||
6,
|
||||
12,
|
||||
21,
|
||||
20,
|
||||
23,
|
||||
25,
|
||||
24,
|
||||
32,
|
||||
50,
|
||||
28,
|
||||
24,
|
||||
27,
|
||||
17,
|
||||
11,
|
||||
6,
|
||||
2,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 589,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 49.081,
|
||||
"low_1pct_fps": 8.19,
|
||||
"low_01pct_fps": 6.325,
|
||||
"p50_ms": 15.443,
|
||||
"p95_ms": 53.004,
|
||||
"p99_ms": 94.69,
|
||||
"p999_ms": 158.114,
|
||||
"max_ms": 158.114,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
7,
|
||||
20,
|
||||
30,
|
||||
46,
|
||||
53,
|
||||
48,
|
||||
56,
|
||||
62,
|
||||
61,
|
||||
66,
|
||||
63,
|
||||
18,
|
||||
22,
|
||||
20,
|
||||
10,
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 2731,
|
||||
"seconds": 12.004,
|
||||
"avg_fps": 227.513,
|
||||
"low_1pct_fps": 53.473,
|
||||
"low_01pct_fps": 33.042,
|
||||
"p50_ms": 3.801,
|
||||
"p95_ms": 7.955,
|
||||
"p99_ms": 14.177,
|
||||
"p999_ms": 24.479,
|
||||
"max_ms": 38.218,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
526,
|
||||
1157,
|
||||
432,
|
||||
257,
|
||||
151,
|
||||
88,
|
||||
46,
|
||||
32,
|
||||
13,
|
||||
15,
|
||||
6,
|
||||
6,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 527,
|
||||
"seconds": 12.016,
|
||||
"avg_fps": 43.857,
|
||||
"low_1pct_fps": 7.338,
|
||||
"low_01pct_fps": 5.157,
|
||||
"p50_ms": 16.784,
|
||||
"p95_ms": 57.353,
|
||||
"p99_ms": 111.984,
|
||||
"p999_ms": 193.905,
|
||||
"max_ms": 193.905,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
9,
|
||||
16,
|
||||
29,
|
||||
40,
|
||||
61,
|
||||
50,
|
||||
54,
|
||||
52,
|
||||
62,
|
||||
57,
|
||||
35,
|
||||
28,
|
||||
11,
|
||||
13,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 746,
|
||||
"seconds": 12.001,
|
||||
"avg_fps": 62.162,
|
||||
"low_1pct_fps": 14.965,
|
||||
"low_01pct_fps": 12.372,
|
||||
"p50_ms": 13.76,
|
||||
"p95_ms": 35.144,
|
||||
"p99_ms": 57.63,
|
||||
"p999_ms": 80.827,
|
||||
"max_ms": 80.827,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
13,
|
||||
12,
|
||||
41,
|
||||
37,
|
||||
53,
|
||||
67,
|
||||
82,
|
||||
80,
|
||||
99,
|
||||
82,
|
||||
76,
|
||||
58,
|
||||
17,
|
||||
11,
|
||||
12,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 3910,
|
||||
"seconds": 12.002,
|
||||
"avg_fps": 325.77,
|
||||
"low_1pct_fps": 79.024,
|
||||
"low_01pct_fps": 41.466,
|
||||
"p50_ms": 2.685,
|
||||
"p95_ms": 5.125,
|
||||
"p99_ms": 8.342,
|
||||
"p999_ms": 20.782,
|
||||
"max_ms": 30.571,
|
||||
"histogram": [
|
||||
0,
|
||||
78,
|
||||
2526,
|
||||
892,
|
||||
203,
|
||||
95,
|
||||
49,
|
||||
27,
|
||||
13,
|
||||
11,
|
||||
7,
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 1268,
|
||||
"seconds": 12.044,
|
||||
"avg_fps": 105.282,
|
||||
"low_1pct_fps": 14.736,
|
||||
"low_01pct_fps": 8.855,
|
||||
"p50_ms": 6.542,
|
||||
"p95_ms": 25.245,
|
||||
"p99_ms": 50.465,
|
||||
"p999_ms": 79.065,
|
||||
"max_ms": 146.809,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
171,
|
||||
190,
|
||||
169,
|
||||
167,
|
||||
133,
|
||||
105,
|
||||
93,
|
||||
53,
|
||||
43,
|
||||
35,
|
||||
41,
|
||||
26,
|
||||
13,
|
||||
11,
|
||||
10,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 822,
|
||||
"seconds": 12.011,
|
||||
"avg_fps": 68.44,
|
||||
"low_1pct_fps": 14.632,
|
||||
"low_01pct_fps": 10.101,
|
||||
"p50_ms": 11.396,
|
||||
"p95_ms": 35.387,
|
||||
"p99_ms": 55.24,
|
||||
"p999_ms": 99.005,
|
||||
"max_ms": 99.005,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
42,
|
||||
62,
|
||||
55,
|
||||
86,
|
||||
89,
|
||||
97,
|
||||
64,
|
||||
73,
|
||||
74,
|
||||
72,
|
||||
48,
|
||||
21,
|
||||
17,
|
||||
8,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 4165,
|
||||
"seconds": 12.006,
|
||||
"avg_fps": 346.896,
|
||||
"low_1pct_fps": 75.429,
|
||||
"low_01pct_fps": 33.579,
|
||||
"p50_ms": 2.541,
|
||||
"p95_ms": 4.838,
|
||||
"p99_ms": 7.998,
|
||||
"p999_ms": 20.209,
|
||||
"max_ms": 53.577,
|
||||
"histogram": [
|
||||
0,
|
||||
640,
|
||||
2307,
|
||||
855,
|
||||
178,
|
||||
93,
|
||||
34,
|
||||
22,
|
||||
15,
|
||||
3,
|
||||
3,
|
||||
8,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 851,
|
||||
"seconds": 12.0,
|
||||
"avg_fps": 70.916,
|
||||
"low_1pct_fps": 11.158,
|
||||
"low_01pct_fps": 7.903,
|
||||
"p50_ms": 10.128,
|
||||
"p95_ms": 36.498,
|
||||
"p99_ms": 68.434,
|
||||
"p999_ms": 126.541,
|
||||
"max_ms": 126.541,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
38,
|
||||
61,
|
||||
87,
|
||||
76,
|
||||
69,
|
||||
84,
|
||||
100,
|
||||
63,
|
||||
53,
|
||||
68,
|
||||
53,
|
||||
48,
|
||||
15,
|
||||
9,
|
||||
16,
|
||||
9,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "vanilla-macmini"
|
||||
}
|
||||
@@ -1,771 +0,0 @@
|
||||
{
|
||||
"format": "horizon-benchmark-report",
|
||||
"version": 1,
|
||||
"kind": "game",
|
||||
"created": "2026-11-04T13:08:37Z",
|
||||
"app": {
|
||||
"name": "minecraft",
|
||||
"version": "26.3",
|
||||
"mods": [
|
||||
{
|
||||
"name": "fabric-loader",
|
||||
"version": "0.19.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hardware": {
|
||||
"os": "macos",
|
||||
"arch": "aarch64",
|
||||
"cpu_threads": 14,
|
||||
"os_version": "27.0.1",
|
||||
"cpu": "Apple M4 Pro",
|
||||
"ram_mb": 65536,
|
||||
"gpu": {
|
||||
"name": "Apple M4 Pro",
|
||||
"vendor": "Apple",
|
||||
"driver": "4.1 Metal - 91.7",
|
||||
"backend": "opengl",
|
||||
"kind": ""
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
"scenario": "perf_bench:village,forest,ocean,mountain",
|
||||
"seed": "1234",
|
||||
"render_distance": 32,
|
||||
"simulation_distance": 12,
|
||||
"vsync": false,
|
||||
"offscreen": false,
|
||||
"repeats": 3,
|
||||
"warmup_s": 2.0,
|
||||
"load_avg_1m": 18.159,
|
||||
"window": [
|
||||
1280,
|
||||
720
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"startup.title_ms": {
|
||||
"n": 3,
|
||||
"mean": 8607.88,
|
||||
"stdev": 949.596,
|
||||
"min": 8010.323,
|
||||
"max": 9702.854,
|
||||
"ci95": 2359.119
|
||||
},
|
||||
"startup.world_ms": {
|
||||
"n": 3,
|
||||
"mean": 44182.88,
|
||||
"stdev": 4031.933,
|
||||
"min": 40172.323,
|
||||
"max": 48235.854,
|
||||
"ci95": 10016.685
|
||||
},
|
||||
"chunks.village.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 17227.171,
|
||||
"stdev": 5351.234,
|
||||
"min": 13892.279,
|
||||
"max": 23399.566,
|
||||
"ci95": 13294.274
|
||||
},
|
||||
"fps.village.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 76.237,
|
||||
"stdev": 45.661,
|
||||
"min": 28.922,
|
||||
"max": 120.041,
|
||||
"ci95": 113.437
|
||||
},
|
||||
"fps.village.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 17.319,
|
||||
"stdev": 12.99,
|
||||
"min": 5.593,
|
||||
"max": 31.283,
|
||||
"ci95": 32.272
|
||||
},
|
||||
"fps.village.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 12.642,
|
||||
"stdev": 9.463,
|
||||
"min": 4.308,
|
||||
"max": 22.93,
|
||||
"ci95": 23.51
|
||||
},
|
||||
"memory.village.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 2674.987,
|
||||
"stdev": 30.71,
|
||||
"min": 2641.352,
|
||||
"max": 2701.531,
|
||||
"ci95": 76.294
|
||||
},
|
||||
"tick.village.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.341,
|
||||
"stdev": 0.407,
|
||||
"min": 0.924,
|
||||
"max": 1.737,
|
||||
"ci95": 1.012
|
||||
},
|
||||
"chunks.forest.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 34112.389,
|
||||
"stdev": 19305.487,
|
||||
"min": 22633.094,
|
||||
"max": 56401.09,
|
||||
"ci95": 47961.36
|
||||
},
|
||||
"fps.forest.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 72.848,
|
||||
"stdev": 51.859,
|
||||
"min": 18.307,
|
||||
"max": 121.528,
|
||||
"ci95": 128.837
|
||||
},
|
||||
"fps.forest.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 22.096,
|
||||
"stdev": 20.299,
|
||||
"min": 3.973,
|
||||
"max": 44.03,
|
||||
"ci95": 50.429
|
||||
},
|
||||
"fps.forest.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 16.093,
|
||||
"stdev": 14.608,
|
||||
"min": 3.688,
|
||||
"max": 32.194,
|
||||
"ci95": 36.291
|
||||
},
|
||||
"memory.forest.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 2519.729,
|
||||
"stdev": 82.097,
|
||||
"min": 2426.297,
|
||||
"max": 2580.328,
|
||||
"ci95": 203.957
|
||||
},
|
||||
"tick.forest.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.115,
|
||||
"stdev": 0.411,
|
||||
"min": 0.726,
|
||||
"max": 1.545,
|
||||
"ci95": 1.02
|
||||
},
|
||||
"chunks.ocean.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 19066.516,
|
||||
"stdev": 9372.33,
|
||||
"min": 13163.781,
|
||||
"max": 29873.383,
|
||||
"ci95": 23284.039
|
||||
},
|
||||
"fps.ocean.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 69.735,
|
||||
"stdev": 46.656,
|
||||
"min": 16.524,
|
||||
"max": 103.635,
|
||||
"ci95": 115.909
|
||||
},
|
||||
"fps.ocean.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 17.636,
|
||||
"stdev": 13.616,
|
||||
"min": 3.179,
|
||||
"max": 30.217,
|
||||
"ci95": 33.827
|
||||
},
|
||||
"fps.ocean.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 12.115,
|
||||
"stdev": 10.798,
|
||||
"min": 2.584,
|
||||
"max": 23.842,
|
||||
"ci95": 26.825
|
||||
},
|
||||
"memory.ocean.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 2557.143,
|
||||
"stdev": 84.375,
|
||||
"min": 2463.781,
|
||||
"max": 2627.945,
|
||||
"ci95": 209.616
|
||||
},
|
||||
"tick.ocean.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 1.056,
|
||||
"stdev": 0.233,
|
||||
"min": 0.815,
|
||||
"max": 1.281,
|
||||
"ci95": 0.58
|
||||
},
|
||||
"chunks.mountain.load_ms": {
|
||||
"n": 3,
|
||||
"mean": 62214.712,
|
||||
"stdev": 4884.13,
|
||||
"min": 58937.045,
|
||||
"max": 67828.14,
|
||||
"ci95": 12133.83
|
||||
},
|
||||
"fps.mountain.avg_fps": {
|
||||
"n": 3,
|
||||
"mean": 51.809,
|
||||
"stdev": 7.118,
|
||||
"min": 45.235,
|
||||
"max": 59.369,
|
||||
"ci95": 17.685
|
||||
},
|
||||
"fps.mountain.low_1pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 6.336,
|
||||
"stdev": 1.097,
|
||||
"min": 5.545,
|
||||
"max": 7.588,
|
||||
"ci95": 2.725
|
||||
},
|
||||
"fps.mountain.low_01pct_fps": {
|
||||
"n": 3,
|
||||
"mean": 3.458,
|
||||
"stdev": 0.756,
|
||||
"min": 2.827,
|
||||
"max": 4.296,
|
||||
"ci95": 1.878
|
||||
},
|
||||
"memory.mountain.rss_mb": {
|
||||
"n": 3,
|
||||
"mean": 2604.227,
|
||||
"stdev": 15.043,
|
||||
"min": 2588.234,
|
||||
"max": 2618.094,
|
||||
"ci95": 37.371
|
||||
},
|
||||
"tick.mountain.client_ms": {
|
||||
"n": 3,
|
||||
"mean": 0.957,
|
||||
"stdev": 0.03,
|
||||
"min": 0.926,
|
||||
"max": 0.985,
|
||||
"ci95": 0.074
|
||||
},
|
||||
"memory.rss_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 2731.016,
|
||||
"stdev": 56.767,
|
||||
"min": 2688.516,
|
||||
"max": 2795.484,
|
||||
"ci95": 141.03
|
||||
},
|
||||
"memory.rss_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 2577.339,
|
||||
"stdev": 77.295,
|
||||
"min": 2488.203,
|
||||
"max": 2625.875,
|
||||
"ci95": 192.028
|
||||
},
|
||||
"memory.footprint_peak_mb": {
|
||||
"n": 3,
|
||||
"mean": 4612.571,
|
||||
"stdev": 20.245,
|
||||
"min": 4595.363,
|
||||
"max": 4634.879,
|
||||
"ci95": 50.296
|
||||
},
|
||||
"memory.footprint_steady_mb": {
|
||||
"n": 3,
|
||||
"mean": 4271.925,
|
||||
"stdev": 26.308,
|
||||
"min": 4246.207,
|
||||
"max": 4298.785,
|
||||
"ci95": 65.357
|
||||
}
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 0,
|
||||
"frames": 349,
|
||||
"seconds": 12.067,
|
||||
"avg_fps": 28.922,
|
||||
"low_1pct_fps": 5.593,
|
||||
"low_01pct_fps": 4.308,
|
||||
"p50_ms": 24.684,
|
||||
"p95_ms": 92.764,
|
||||
"p99_ms": 150.338,
|
||||
"p999_ms": 232.136,
|
||||
"max_ms": 232.136,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
13,
|
||||
17,
|
||||
26,
|
||||
31,
|
||||
40,
|
||||
46,
|
||||
50,
|
||||
32,
|
||||
25,
|
||||
20,
|
||||
30,
|
||||
10,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 1,
|
||||
"frames": 1441,
|
||||
"seconds": 12.004,
|
||||
"avg_fps": 120.041,
|
||||
"low_1pct_fps": 31.283,
|
||||
"low_01pct_fps": 22.93,
|
||||
"p50_ms": 7.267,
|
||||
"p95_ms": 13.622,
|
||||
"p99_ms": 23.449,
|
||||
"p999_ms": 42.564,
|
||||
"max_ms": 44.657,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
64,
|
||||
543,
|
||||
397,
|
||||
218,
|
||||
100,
|
||||
54,
|
||||
24,
|
||||
20,
|
||||
7,
|
||||
7,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "village",
|
||||
"repeat": 2,
|
||||
"frames": 958,
|
||||
"seconds": 12.013,
|
||||
"avg_fps": 79.749,
|
||||
"low_1pct_fps": 15.082,
|
||||
"low_01pct_fps": 10.689,
|
||||
"p50_ms": 9.138,
|
||||
"p95_ms": 28.848,
|
||||
"p99_ms": 52.247,
|
||||
"p999_ms": 93.55,
|
||||
"max_ms": 93.55,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
19,
|
||||
144,
|
||||
232,
|
||||
148,
|
||||
111,
|
||||
64,
|
||||
56,
|
||||
59,
|
||||
57,
|
||||
32,
|
||||
17,
|
||||
9,
|
||||
6,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 0,
|
||||
"frames": 220,
|
||||
"seconds": 12.017,
|
||||
"avg_fps": 18.307,
|
||||
"low_1pct_fps": 3.973,
|
||||
"low_01pct_fps": 3.688,
|
||||
"p50_ms": 40.635,
|
||||
"p95_ms": 157.363,
|
||||
"p99_ms": 235.253,
|
||||
"p999_ms": 271.153,
|
||||
"max_ms": 271.153,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
6,
|
||||
14,
|
||||
17,
|
||||
25,
|
||||
25,
|
||||
17,
|
||||
27,
|
||||
33,
|
||||
24,
|
||||
16,
|
||||
11,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 1,
|
||||
"frames": 1459,
|
||||
"seconds": 12.005,
|
||||
"avg_fps": 121.528,
|
||||
"low_1pct_fps": 44.03,
|
||||
"low_01pct_fps": 32.194,
|
||||
"p50_ms": 7.381,
|
||||
"p95_ms": 12.386,
|
||||
"p99_ms": 18.354,
|
||||
"p999_ms": 29.428,
|
||||
"max_ms": 32.695,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
357,
|
||||
642,
|
||||
300,
|
||||
80,
|
||||
28,
|
||||
28,
|
||||
14,
|
||||
7,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "forest",
|
||||
"repeat": 2,
|
||||
"frames": 945,
|
||||
"seconds": 12.006,
|
||||
"avg_fps": 78.709,
|
||||
"low_1pct_fps": 18.284,
|
||||
"low_01pct_fps": 12.397,
|
||||
"p50_ms": 9.554,
|
||||
"p95_ms": 27.657,
|
||||
"p99_ms": 43.253,
|
||||
"p999_ms": 80.663,
|
||||
"max_ms": 80.663,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
18,
|
||||
304,
|
||||
191,
|
||||
119,
|
||||
67,
|
||||
59,
|
||||
53,
|
||||
70,
|
||||
34,
|
||||
15,
|
||||
10,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 0,
|
||||
"frames": 201,
|
||||
"seconds": 12.164,
|
||||
"avg_fps": 16.524,
|
||||
"low_1pct_fps": 3.179,
|
||||
"low_01pct_fps": 2.584,
|
||||
"p50_ms": 44.933,
|
||||
"p95_ms": 155.019,
|
||||
"p99_ms": 263.919,
|
||||
"p999_ms": 387.044,
|
||||
"max_ms": 387.044,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
4,
|
||||
7,
|
||||
20,
|
||||
8,
|
||||
29,
|
||||
18,
|
||||
25,
|
||||
29,
|
||||
29,
|
||||
17,
|
||||
8,
|
||||
4,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 1,
|
||||
"frames": 1244,
|
||||
"seconds": 12.004,
|
||||
"avg_fps": 103.635,
|
||||
"low_1pct_fps": 30.217,
|
||||
"low_01pct_fps": 23.842,
|
||||
"p50_ms": 8.349,
|
||||
"p95_ms": 17.445,
|
||||
"p99_ms": 28.276,
|
||||
"p999_ms": 38.037,
|
||||
"max_ms": 45.85,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
162,
|
||||
453,
|
||||
312,
|
||||
129,
|
||||
71,
|
||||
47,
|
||||
32,
|
||||
18,
|
||||
16,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "ocean",
|
||||
"repeat": 2,
|
||||
"frames": 1069,
|
||||
"seconds": 12.005,
|
||||
"avg_fps": 89.047,
|
||||
"low_1pct_fps": 19.512,
|
||||
"low_01pct_fps": 9.92,
|
||||
"p50_ms": 9.541,
|
||||
"p95_ms": 20.819,
|
||||
"p99_ms": 31.409,
|
||||
"p999_ms": 99.668,
|
||||
"max_ms": 101.942,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
64,
|
||||
266,
|
||||
283,
|
||||
160,
|
||||
112,
|
||||
74,
|
||||
51,
|
||||
33,
|
||||
17,
|
||||
4,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 0,
|
||||
"frames": 551,
|
||||
"seconds": 12.181,
|
||||
"avg_fps": 45.235,
|
||||
"low_1pct_fps": 5.545,
|
||||
"low_01pct_fps": 3.252,
|
||||
"p50_ms": 14.921,
|
||||
"p95_ms": 50.78,
|
||||
"p99_ms": 105.154,
|
||||
"p999_ms": 307.476,
|
||||
"max_ms": 307.476,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
42,
|
||||
137,
|
||||
72,
|
||||
58,
|
||||
50,
|
||||
47,
|
||||
57,
|
||||
39,
|
||||
20,
|
||||
15,
|
||||
8,
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 1,
|
||||
"frames": 718,
|
||||
"seconds": 12.094,
|
||||
"avg_fps": 59.369,
|
||||
"low_1pct_fps": 7.588,
|
||||
"low_01pct_fps": 4.296,
|
||||
"p50_ms": 12.178,
|
||||
"p95_ms": 38.273,
|
||||
"p99_ms": 65.694,
|
||||
"p999_ms": 232.789,
|
||||
"max_ms": 232.789,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
140,
|
||||
202,
|
||||
115,
|
||||
72,
|
||||
48,
|
||||
47,
|
||||
43,
|
||||
19,
|
||||
19,
|
||||
6,
|
||||
4,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "mountain",
|
||||
"repeat": 2,
|
||||
"frames": 610,
|
||||
"seconds": 12.003,
|
||||
"avg_fps": 50.822,
|
||||
"low_1pct_fps": 5.875,
|
||||
"low_01pct_fps": 2.827,
|
||||
"p50_ms": 13.894,
|
||||
"p95_ms": 44.208,
|
||||
"p99_ms": 75.023,
|
||||
"p999_ms": 353.686,
|
||||
"max_ms": 353.686,
|
||||
"histogram": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
67,
|
||||
115,
|
||||
125,
|
||||
88,
|
||||
55,
|
||||
54,
|
||||
43,
|
||||
25,
|
||||
13,
|
||||
15,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "vanilla-macmini"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# `keys.contains(&KeyCode::ShiftLeft)` reads a mapping by its default key: ask by `Action`, so the
|
||||
# mapping is named and every device that can hold it counts.
|
||||
disallowed-methods = [
|
||||
{ path = "horizon_client_input::held::HeldKeys::contains", reason = "use `held(Action::..)`, or `raw` for a key that is no mapping's default", allow-invalid = true },
|
||||
]
|
||||
@@ -3,17 +3,9 @@ name = "cargo-horizon-mod"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
rust-version = "1.86"
|
||||
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
|
||||
|
||||
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120) reads this: the archive
|
||||
# `cargo-horizon-mod`'s workflow attaches to the `cargo-horizon-mod-v<version>` release for the runner's target,
|
||||
# holding the binary at its root. tools/horizon_mod_binstall.py expands it for the workflow's targets.
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "https://github.com/horizon-rust/horizon/releases/download/cargo-horizon-mod-v{ version }/{ name }-{ target }.tar.gz"
|
||||
bin-dir = "{ bin }{ binary-ext }"
|
||||
pkg-fmt = "tgz"
|
||||
|
||||
[dependencies]
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
|
||||
serde_json = "1.0.151"
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn kind_of(path: &Path) -> Option<Kind> {
|
||||
"hmod" => Some(Kind::Mod),
|
||||
"hnmod" => Some(Kind::NativeMod),
|
||||
"hpack" => Some(Kind::Modpack),
|
||||
"zip" => Some(horizon_mods::index::zip_kind(path)),
|
||||
"zip" => Some(Kind::ResourcePack),
|
||||
"hshader" => Some(Kind::Shader),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "horizon-anticheat"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
rust-version = "1.85"
|
||||
description = "The dedicated server's anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -34,10 +34,7 @@ pub fn element(v: &Element) -> Value {
|
||||
.collect();
|
||||
json!({"from":bits(v.from),"to":bits(v.to),"faces":faces,"rotation":v.rotation.as_ref().map(rotation),"shade":v.shade_direction_override.map(Direction::name),"light":v.light_emission})
|
||||
}
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "shared by fixtures which normalize different model stages"
|
||||
)]
|
||||
#[allow(dead_code)] // Shared by fixtures which normalize different model stages.
|
||||
pub fn model(v: &CuboidModel) -> Value {
|
||||
let textures: BTreeMap<_, _> = v
|
||||
.textures
|
||||
|
||||
@@ -34,8 +34,6 @@ pub struct ModelQuad {
|
||||
/// NeoForge's `render_type` names.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RenderType {
|
||||
/// A declared render type unknown to this baker; distinct from an absent default.
|
||||
Unsupported,
|
||||
Solid,
|
||||
Cutout,
|
||||
CutoutMipped,
|
||||
@@ -236,10 +234,7 @@ fn bake_into(
|
||||
skip_flat: bool,
|
||||
output: &mut Vec<ModelQuad>,
|
||||
) -> Result<()> {
|
||||
let render_type = model
|
||||
.render_type
|
||||
.as_deref()
|
||||
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
|
||||
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
|
||||
for element in model.elements() {
|
||||
let element = element
|
||||
.as_ref()
|
||||
@@ -297,10 +292,7 @@ fn bake_obj(
|
||||
transform: BlockModelTransform,
|
||||
output: &mut Vec<ModelQuad>,
|
||||
) -> Result<()> {
|
||||
let render_type = model
|
||||
.render_type
|
||||
.as_deref()
|
||||
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
|
||||
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
|
||||
for face in obj.faces.iter().filter(|f| !hidden.contains(&f.object)) {
|
||||
let slot = match obj.materials.get(&face.material) {
|
||||
Some(MtlTexture::Sprite(sprite)) => {
|
||||
|
||||
@@ -179,19 +179,19 @@ impl Condition {
|
||||
let fields = object(raw)?;
|
||||
if fields.len() == 1 {
|
||||
for name in ["OR", "AND"] {
|
||||
if let Some(raw) = fields.get(name)
|
||||
&& let Ok(values) = array(raw).and_then(|values| {
|
||||
if let Some(raw) = fields.get(name) {
|
||||
if let Ok(values) = array(raw).and_then(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(Self::decode)
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
{
|
||||
return Ok(if name == "OR" {
|
||||
Self::Or(values)
|
||||
} else {
|
||||
Self::And(values)
|
||||
});
|
||||
}) {
|
||||
return Ok(if name == "OR" {
|
||||
Self::Or(values)
|
||||
} else {
|
||||
Self::And(values)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +108,6 @@ impl<T> CompiledChoices<T> {
|
||||
multipart: self.multipart.iter().map(|c| c.map(&mut f)).collect(),
|
||||
}
|
||||
}
|
||||
/// A variant that needs no random selection and produces exactly one model.
|
||||
pub fn single_variant(&self, selected: &SelectedModels) -> Option<&T> {
|
||||
let SelectedModels::Variant(index) = selected else {
|
||||
return None;
|
||||
};
|
||||
match self.variants.get(*index)? {
|
||||
Choice::Single(value) => Some(value),
|
||||
Choice::Weighted { .. } => None,
|
||||
}
|
||||
}
|
||||
/// The variant `particleMaterial` comes from: the first entry of a weighted
|
||||
/// list (`WeightedVariants`) and, for a multipart root, of its first
|
||||
/// selector whether or not that selector applies (`MultiPartModel`).
|
||||
|
||||
@@ -917,46 +917,6 @@ impl NumericProperty {
|
||||
/// (`None` removes a prototype component).
|
||||
pub type Components = BTreeMap<String, Value>;
|
||||
|
||||
/// A stack's components read in place: its patch first, then its item's defaults. Reading one copies
|
||||
/// nothing, where [`ItemModels::components`] clones the defaults for a stack with a patch (an enchanted
|
||||
/// sword's are a few dozen JSON values, on every draw of it).
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ComponentsView<'a> {
|
||||
prototype: &'a Components,
|
||||
patch: &'a BTreeMap<String, Option<Value>>,
|
||||
}
|
||||
|
||||
impl<'a> ComponentsView<'a> {
|
||||
pub fn get(&self, key: &str) -> Option<&'a Value> {
|
||||
match self.patch.get(key) {
|
||||
Some(patched) => patched.as_ref(),
|
||||
None => self.prototype.get(key),
|
||||
}
|
||||
}
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.get(key).is_some()
|
||||
}
|
||||
/// Components with no patch: `components` as they are.
|
||||
pub fn of(components: &'a Components) -> Self {
|
||||
static NO_PATCH: BTreeMap<String, Option<Value>> = BTreeMap::new();
|
||||
Self {
|
||||
prototype: components,
|
||||
patch: &NO_PATCH,
|
||||
}
|
||||
}
|
||||
/// The merged map, for what needs one whole.
|
||||
pub fn to_map(&self) -> Components {
|
||||
let mut out = self.prototype.clone();
|
||||
for (key, value) in self.patch {
|
||||
match value {
|
||||
Some(v) => out.insert(key.clone(), v.clone()),
|
||||
None => out.remove(key),
|
||||
};
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// An item stack as the resolver reads it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct StackView {
|
||||
@@ -1127,15 +1087,14 @@ impl ItemModels {
|
||||
if stack.patch.is_empty() {
|
||||
return std::borrow::Cow::Borrowed(prototype);
|
||||
}
|
||||
std::borrow::Cow::Owned(self.components_view(stack).to_map())
|
||||
}
|
||||
/// The stack's components, read in place (see [`ComponentsView`]).
|
||||
pub fn components_view<'a>(&'a self, stack: &'a StackView) -> ComponentsView<'a> {
|
||||
static NONE: Components = BTreeMap::new();
|
||||
ComponentsView {
|
||||
prototype: self.prototypes.get(&stack.item).unwrap_or(&NONE),
|
||||
patch: &stack.patch,
|
||||
let mut out = prototype.clone();
|
||||
for (key, value) in &stack.patch {
|
||||
match value {
|
||||
Some(v) => out.insert(key.clone(), v.clone()),
|
||||
None => out.remove(key),
|
||||
};
|
||||
}
|
||||
std::borrow::Cow::Owned(out)
|
||||
}
|
||||
/// `ItemModelResolver.updateForTopItem`.
|
||||
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
|
||||
@@ -1153,7 +1112,7 @@ impl ItemModels {
|
||||
ctx: &Context,
|
||||
env: &dyn ItemEnv,
|
||||
) {
|
||||
let components = self.components_view(stack);
|
||||
let components = self.components(stack);
|
||||
let Some(Value::String(model_id)) = components.get("minecraft:item_model") else {
|
||||
return;
|
||||
};
|
||||
@@ -1163,7 +1122,7 @@ impl ItemModels {
|
||||
let mut state = Resolve {
|
||||
models: self,
|
||||
stack,
|
||||
components,
|
||||
components: &components,
|
||||
ctx,
|
||||
env,
|
||||
};
|
||||
@@ -1181,7 +1140,7 @@ impl ItemModels {
|
||||
self.properties(stack).2
|
||||
}
|
||||
fn properties(&self, stack: &StackView) -> (bool, bool, f32) {
|
||||
match self.components_view(stack).get("minecraft:item_model") {
|
||||
match self.components(stack).get("minecraft:item_model") {
|
||||
Some(Value::String(id)) => identifier(id)
|
||||
.ok()
|
||||
.and_then(|id| self.definitions.get(&id))
|
||||
@@ -1200,7 +1159,7 @@ impl ItemModels {
|
||||
struct Resolve<'a> {
|
||||
models: &'a ItemModels,
|
||||
stack: &'a StackView,
|
||||
components: ComponentsView<'a>,
|
||||
components: &'a Components,
|
||||
ctx: &'a Context<'a>,
|
||||
env: &'a dyn ItemEnv,
|
||||
}
|
||||
@@ -1496,9 +1455,9 @@ impl Resolve<'_> {
|
||||
fn numeric(&self, property: &NumericProperty) -> f32 {
|
||||
let owner = self.ctx.owner;
|
||||
match property {
|
||||
NumericProperty::BundleFullness => self
|
||||
.env
|
||||
.bundle_fullness(self.stack, &self.components.to_map()),
|
||||
NumericProperty::BundleFullness => {
|
||||
self.env.bundle_fullness(self.stack, self.components)
|
||||
}
|
||||
NumericProperty::Compass {
|
||||
wobble,
|
||||
target,
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(super) fn decode(m: &Map<String, Value>) -> Result<Value> {
|
||||
/// with a matching level. An empty predicate list still needs the component.
|
||||
pub(super) fn matches(
|
||||
predicate: &Value,
|
||||
components: ComponentsView<'_>,
|
||||
components: &Components,
|
||||
env: &dyn ItemEnv,
|
||||
) -> Option<bool> {
|
||||
let (id, expected) = predicate.as_object()?.iter().next()?;
|
||||
@@ -143,17 +143,13 @@ mod tests {
|
||||
json!({"minecraft:density":0}),
|
||||
)]);
|
||||
assert_eq!(
|
||||
matches(
|
||||
&json!({"minecraft:enchantments":[{}]}),
|
||||
ComponentsView::of(&components),
|
||||
&NoEnv
|
||||
),
|
||||
matches(&json!({"minecraft:enchantments":[{}]}), &components, &NoEnv),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
matches(
|
||||
&json!({"minecraft:enchantments":[{"enchantments":"minecraft:density"}]}),
|
||||
ComponentsView::of(&components),
|
||||
&components,
|
||||
&NoEnv
|
||||
),
|
||||
Some(false)
|
||||
@@ -170,21 +166,12 @@ mod tests {
|
||||
"minecraft:enchantments".into(),
|
||||
json!({"minecraft:density":3}),
|
||||
)]);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &Tags),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &NoEnv),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(matches(&predicate, &components, &Tags), Some(true));
|
||||
assert_eq!(matches(&predicate, &components, &NoEnv), Some(false));
|
||||
components.insert(
|
||||
"minecraft:enchantments".into(),
|
||||
json!({"minecraft:density":2}),
|
||||
);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &Tags),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(matches(&predicate, &components, &Tags), Some(false));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn identifier_adds_default_namespace() {
|
||||
@@ -112,49 +111,3 @@ fn custom_condition_validates_its_name_and_both_branches() {
|
||||
assert!(matches!(node("example:charged", empty).unwrap(),
|
||||
ItemModelNode::Condition { property: ConditionProperty::Custom(name), .. } if name == "example:charged"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
|
||||
let mut models = ItemModels::default();
|
||||
models.prototypes.insert(
|
||||
"minecraft:diamond_sword".into(),
|
||||
Components::from_iter([
|
||||
(
|
||||
"minecraft:item_model".into(),
|
||||
json!("minecraft:diamond_sword"),
|
||||
),
|
||||
("minecraft:max_damage".into(), json!(1561)),
|
||||
("minecraft:rarity".into(), json!("common")),
|
||||
]),
|
||||
);
|
||||
let mut stack = StackView::new("minecraft:diamond_sword", 1);
|
||||
stack.patch.insert(
|
||||
"minecraft:enchantments".into(),
|
||||
Some(json!({"minecraft:sharpness": 5})),
|
||||
);
|
||||
stack
|
||||
.patch
|
||||
.insert("minecraft:rarity".into(), Some(json!("rare")));
|
||||
stack.patch.insert("minecraft:max_damage".into(), None);
|
||||
let view = models.components_view(&stack);
|
||||
assert_eq!(view.get("minecraft:rarity"), Some(&json!("rare")));
|
||||
assert_eq!(view.get("minecraft:max_damage"), None, "a removed default");
|
||||
assert_eq!(
|
||||
view.get("minecraft:item_model"),
|
||||
Some(&json!("minecraft:diamond_sword"))
|
||||
);
|
||||
assert!(view.contains_key("minecraft:enchantments"));
|
||||
assert!(!view.contains_key("minecraft:custom_name"));
|
||||
// Every key reads as the merged map has it.
|
||||
let merged = models.components(&stack);
|
||||
for key in [
|
||||
"minecraft:rarity",
|
||||
"minecraft:max_damage",
|
||||
"minecraft:item_model",
|
||||
"minecraft:enchantments",
|
||||
"minecraft:custom_name",
|
||||
] {
|
||||
assert_eq!(view.get(key), merged.get(key), "{key}");
|
||||
}
|
||||
assert_eq!(view.to_map(), *merged);
|
||||
}
|
||||
@@ -290,12 +290,12 @@ pub fn normalize_quaternion(q: [f32; 4]) -> [f32; 4] {
|
||||
[x * inv, y * inv, z * inv, w * inv]
|
||||
}
|
||||
/// JOML `Math.sin(float)` without fast math.
|
||||
pub fn joml_sin(rad: f32) -> f32 {
|
||||
fn joml_sin(rad: f32) -> f32 {
|
||||
f64::from(rad).sin() as f32
|
||||
}
|
||||
/// JOML `Math.cosFromSin(float, float)` without fast math: the magnitude from
|
||||
/// the sine, the sign from the angle's quadrant (exact zeros at right angles).
|
||||
pub fn cos_from_sin(sin: f32, angle: f32) -> f32 {
|
||||
fn cos_from_sin(sin: f32, angle: f32) -> f32 {
|
||||
let cos = (1.0 - sin * sin).sqrt();
|
||||
let tau = (std::f64::consts::PI * 2.0) as f32;
|
||||
let a = angle + std::f32::consts::FRAC_PI_2;
|
||||
|
||||
@@ -777,23 +777,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn an_unknown_declared_render_type_remains_distinct_from_absent() {
|
||||
let json = LIGHTS.replace("minecraft:cutout", "fixture:custom_material");
|
||||
let library = library(&[("block/custom", &json)]);
|
||||
let model = library.resolve("block/custom").unwrap();
|
||||
let quads = crate::bake::bake_block_model(
|
||||
&model,
|
||||
crate::bake::BlockModelTransform::new([0; 3], false),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!quads.is_empty());
|
||||
assert!(
|
||||
quads
|
||||
.iter()
|
||||
.all(|q| q.render_type == Some(crate::bake::RenderType::Unsupported))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn render_type_comes_from_the_leaf_most_model_and_loses_its_prefix() {
|
||||
let library = library(&[
|
||||
("block/base", r#"{"render_type":"translucent"}"#),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name = "horizon-bench"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
license = "MIT"
|
||||
description = "The benchmark report format (docs/benchmark-report.md), shared by the game, the launcher and the upload"
|
||||
|
||||
|
||||
@@ -90,9 +90,6 @@ horizon-bots: load-test bots for a Minecraft Java Edition 26.3 server (offline m
|
||||
--seed N default 12345
|
||||
--explore-speed B blocks a second an explore bot flies; default 22 (as fast as chunks come)
|
||||
--pvp B fight bots also attack each other (true or false); default true
|
||||
--crits fight bots swing at a sword's full strength (every 13 ticks) and jump into every
|
||||
other swing, landing it while falling: a critical hit, then a sweep from the
|
||||
ground (give them swords); for PvP benches (crit and sweep particles)
|
||||
--name PREFIX bot names: PREFIX0, PREFIX1...; default Bot
|
||||
--brand B the client brand sent in configuration (minecraft:brand); default none
|
||||
--height H blocks above spawn the bots fly at (walk, explore, idle); default 24
|
||||
@@ -168,8 +165,6 @@ struct Options {
|
||||
attack_after: f64,
|
||||
attack_rounds: u32,
|
||||
no_swing: bool,
|
||||
/// --crits: fight bots alternate jumped critical hits and sweeps at a sword's cooldown.
|
||||
crits: bool,
|
||||
flood: usize,
|
||||
no_read: bool,
|
||||
no_loaded: bool,
|
||||
@@ -207,7 +202,6 @@ fn options() -> Result<Options, String> {
|
||||
attack_after: 5.0,
|
||||
attack_rounds: u32::MAX,
|
||||
no_swing: false,
|
||||
crits: false,
|
||||
flood: 100,
|
||||
no_read: false,
|
||||
no_loaded: false,
|
||||
@@ -236,10 +230,6 @@ fn options() -> Result<Options, String> {
|
||||
o.no_swing = true;
|
||||
continue;
|
||||
}
|
||||
if arg == "--crits" {
|
||||
o.crits = true;
|
||||
continue;
|
||||
}
|
||||
if arg == "--no-loaded" {
|
||||
o.no_loaded = true;
|
||||
continue;
|
||||
@@ -542,84 +532,11 @@ fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
|
||||
}
|
||||
|
||||
/// A sword's attack speed is 1.6 a second: full strength 20 / 1.6 = 12.5 ticks after a swing.
|
||||
const SWORD_COOLDOWN_TICKS: u32 = 13;
|
||||
/// `LivingEntity.getJumpPower`: a jump's first upward motion, in blocks a tick.
|
||||
const JUMP_POWER: f64 = 0.42;
|
||||
|
||||
/// `--crits`: a fight bot that swings at a sword's full strength and jumps into every other swing, so
|
||||
/// it lands while falling (`Player.attack`'s critical hit: falling, not on the ground, full strength),
|
||||
/// and the next one from the ground (a sweep: on the ground, full strength, a sword).
|
||||
#[derive(Default)]
|
||||
struct Crits {
|
||||
/// Vertical motion while in the air (`LivingEntity.travel`: minus 0.08 a tick, times 0.98).
|
||||
jump: Option<f64>,
|
||||
ground: f64,
|
||||
/// The next swing is the jumped one.
|
||||
crit_next: bool,
|
||||
}
|
||||
|
||||
impl Crits {
|
||||
fn airborne(&self) -> bool {
|
||||
self.jump.is_some()
|
||||
}
|
||||
|
||||
/// One tick: walk up to the target and, with a full cooldown, strike; jump ahead of a crit's swing.
|
||||
fn tick(
|
||||
&mut self,
|
||||
pos: &mut [f64; 3],
|
||||
target: (i32, [f64; 3]),
|
||||
since: &mut u32,
|
||||
) -> Option<i32> {
|
||||
if let Some(v) = self.jump {
|
||||
pos[1] += v;
|
||||
if pos[1] <= self.ground {
|
||||
pos[1] = self.ground;
|
||||
self.jump = None;
|
||||
} else {
|
||||
self.jump = Some((v - 0.08) * 0.98);
|
||||
}
|
||||
} else {
|
||||
self.ground = pos[1];
|
||||
// A jump takes 6 ticks to its top: start it so the cooldown ends on the way down.
|
||||
if self.crit_next && *since + 8 >= SWORD_COOLDOWN_TICKS {
|
||||
self.jump = Some(JUMP_POWER);
|
||||
}
|
||||
}
|
||||
let falling = self.jump.is_some_and(|v| v < 0.0);
|
||||
if self.crit_next && !falling {
|
||||
*since = since.saturating_add(1);
|
||||
fight_step(pos, target.1);
|
||||
return None;
|
||||
}
|
||||
let hit = fight_tick(pos, target, since, SWORD_COOLDOWN_TICKS);
|
||||
if hit.is_some() {
|
||||
self.crit_next = !self.crit_next;
|
||||
}
|
||||
hit
|
||||
}
|
||||
}
|
||||
|
||||
/// One tick of a fighting bot with a target: walk up to it, and strike once the cooldown is over.
|
||||
/// Returns the entity to attack. `since_attack` counts ticks since the last swing.
|
||||
fn fight_tick(
|
||||
pos: &mut [f64; 3],
|
||||
target: (i32, [f64; 3]),
|
||||
since_attack: &mut u32,
|
||||
cooldown: u32,
|
||||
) -> Option<i32> {
|
||||
fn fight_tick(pos: &mut [f64; 3], target: (i32, [f64; 3]), since_attack: &mut u32) -> Option<i32> {
|
||||
*since_attack = since_attack.saturating_add(1);
|
||||
let (id, at) = target;
|
||||
fight_step(pos, at);
|
||||
if *since_attack >= cooldown && distance(*pos, at) <= ATTACK_REACH {
|
||||
*since_attack = 0;
|
||||
return Some(id);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Walk toward `at`, stopping `CLOSE_IN` short of it.
|
||||
fn fight_step(pos: &mut [f64; 3], at: [f64; 3]) {
|
||||
let (dx, dz) = (at[0] - pos[0], at[2] - pos[2]);
|
||||
let horizontal = dx.hypot(dz);
|
||||
if horizontal > CLOSE_IN {
|
||||
@@ -627,6 +544,11 @@ fn fight_step(pos: &mut [f64; 3], at: [f64; 3]) {
|
||||
pos[0] += dx / horizontal * step;
|
||||
pos[2] += dz / horizontal * step;
|
||||
}
|
||||
if *since_attack >= ATTACK_COOLDOWN_TICKS && distance(*pos, at) <= ATTACK_REACH {
|
||||
*since_attack = 0;
|
||||
return Some(id);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// One tick of walking to random points around the spawn, at sprint speed (5.6 blocks a second).
|
||||
@@ -694,7 +616,7 @@ fn costly(
|
||||
Behaviour::FloodUse => {
|
||||
// A block within reach: place stone there, or break what is there.
|
||||
let at = [0, 1, 2].map(|k| block[k] + (random.unit() * 7.0) as i32 - 3);
|
||||
if i.is_multiple_of(2) {
|
||||
if i % 2 == 0 {
|
||||
play::Serverbound::UseItemOn(play::UseItemOn {
|
||||
hand: 0,
|
||||
pos: at,
|
||||
@@ -723,7 +645,7 @@ fn costly(
|
||||
carried: None,
|
||||
}),
|
||||
Behaviour::FloodSuggest => {
|
||||
let text = if i.is_multiple_of(2) {
|
||||
let text = if i % 2 == 0 {
|
||||
"/"
|
||||
} else {
|
||||
LONG_COMMAND.as_str()
|
||||
@@ -1202,7 +1124,6 @@ fn run_bot(
|
||||
spawn[1] + o.height
|
||||
};
|
||||
let mut since_attack = ATTACK_COOLDOWN_TICKS;
|
||||
let mut crits = Crits::default();
|
||||
let heading = random.unit() * std::f64::consts::TAU;
|
||||
let mut target = pos;
|
||||
let mut next = Instant::now();
|
||||
@@ -1260,16 +1181,11 @@ fn run_bot(
|
||||
Behaviour::Fight => {
|
||||
let near = lock(&bot.entities).nearest(pos, o.pvp, &bots);
|
||||
if let Some(near) = near {
|
||||
let hit = if o.crits {
|
||||
crits.tick(&mut pos, near, &mut since_attack)
|
||||
} else {
|
||||
fight_tick(&mut pos, near, &mut since_attack, ATTACK_COOLDOWN_TICKS)
|
||||
};
|
||||
if let Some(victim) = hit {
|
||||
if let Some(victim) = fight_tick(&mut pos, near, &mut since_attack) {
|
||||
// The client sends the attack, then swings the arm.
|
||||
let sent = send(
|
||||
play::Serverbound::Attack(play::Attack { id: victim }).encode(),
|
||||
) && send(play::Serverbound::Punch(Empty).encode());
|
||||
) && send(raw(sb::PUNCH, |w| w.varint(0)));
|
||||
if sent {
|
||||
bot.attacks.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
@@ -1384,10 +1300,9 @@ fn run_bot(
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
z: pos[2],
|
||||
// Fight bots stand on the ground (but in a --crits jump); the others fly.
|
||||
// Fight bots stand on the ground; the others fly.
|
||||
flags: u8::from(
|
||||
(matches!(behaviour, Behaviour::Fight | Behaviour::Stand) && !crits.airborne())
|
||||
|| o.ground,
|
||||
matches!(behaviour, Behaviour::Fight | Behaviour::Stand) || o.ground,
|
||||
),
|
||||
})
|
||||
.encode(),
|
||||
@@ -1555,10 +1470,10 @@ fn main() {
|
||||
});
|
||||
if o.report.as_deref() == Some("-") {
|
||||
println!("{summary:#}");
|
||||
} else if let Some(path) = &o.report
|
||||
&& let Err(e) = std::fs::write(path, format!("{summary:#}\n"))
|
||||
{
|
||||
eprintln!("horizon-bots: {path}: {e}");
|
||||
} else if let Some(path) = &o.report {
|
||||
if let Err(e) = std::fs::write(path, format!("{summary:#}\n")) {
|
||||
eprintln!("horizon-bots: {path}: {e}");
|
||||
}
|
||||
}
|
||||
let errors: Vec<String> = bots
|
||||
.iter()
|
||||
@@ -1685,9 +1600,7 @@ mod tests {
|
||||
let target = (3, [1.0, 64.0, 0.0]);
|
||||
let mut since = ATTACK_COOLDOWN_TICKS;
|
||||
let hits: Vec<u32> = (0..40)
|
||||
.filter_map(|tick| {
|
||||
fight_tick(&mut pos, target, &mut since, ATTACK_COOLDOWN_TICKS).map(|_| tick)
|
||||
})
|
||||
.filter_map(|tick| fight_tick(&mut pos, target, &mut since).map(|_| tick))
|
||||
.collect();
|
||||
assert_eq!(hits[0], 0, "a bot in reach strikes at once");
|
||||
assert!(
|
||||
@@ -1704,7 +1617,7 @@ mod tests {
|
||||
let mut since = ATTACK_COOLDOWN_TICKS;
|
||||
let mut first = None;
|
||||
for tick in 0..100 {
|
||||
if fight_tick(&mut pos, target, &mut since, ATTACK_COOLDOWN_TICKS).is_some() {
|
||||
if fight_tick(&mut pos, target, &mut since).is_some() {
|
||||
first = Some(tick);
|
||||
break;
|
||||
}
|
||||
@@ -1714,43 +1627,6 @@ mod tests {
|
||||
assert!(pos[0] < 10.0 - CLOSE_IN + 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crits_alternate_a_falling_swing_and_a_grounded_one() {
|
||||
let mut pos = [0.0, 64.0, 0.0];
|
||||
let target = (3, [1.0, 64.0, 0.0]);
|
||||
let (mut since, mut crits) = (SWORD_COOLDOWN_TICKS, Crits::default());
|
||||
let mut last_y = pos[1];
|
||||
let mut hits = Vec::new();
|
||||
for tick in 0..120u32 {
|
||||
let hit = crits.tick(&mut pos, target, &mut since);
|
||||
if hit.is_some() {
|
||||
hits.push((tick, crits_state(pos[1], last_y)));
|
||||
}
|
||||
last_y = pos[1];
|
||||
}
|
||||
assert!(hits.len() >= 6, "{hits:?}");
|
||||
for (i, w) in hits.windows(2).enumerate() {
|
||||
assert!(
|
||||
w[1].0 - w[0].0 >= SWORD_COOLDOWN_TICKS,
|
||||
"full strength each time: {hits:?}"
|
||||
);
|
||||
assert_ne!(w[0].1, w[1].1, "swing {i} and the next alternate: {hits:?}");
|
||||
}
|
||||
// A crit lands in the air, on the way down; a sweep from the ground.
|
||||
assert!(hits.iter().any(|h| h.1 == "falling") && hits.iter().any(|h| h.1 == "ground"));
|
||||
assert!(pos[1] >= 64.0);
|
||||
}
|
||||
|
||||
fn crits_state(y: f64, before: f64) -> &'static str {
|
||||
if y == 64.0 {
|
||||
"ground"
|
||||
} else if y < before {
|
||||
"falling"
|
||||
} else {
|
||||
"rising"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentiles_use_nearest_rank() {
|
||||
assert_eq!(percentile(&[], 50), 0);
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "horizon-chat-filter"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
rust-version = "1.88"
|
||||
description = "The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -489,36 +489,15 @@ pub fn parse_duration(text: &str) -> Option<i64> {
|
||||
(count > 0).then(|| count.checked_mul(unit)).flatten()
|
||||
}
|
||||
|
||||
/// The unit used when presenting a moderation duration to a player.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum DurationUnit {
|
||||
Minute,
|
||||
Hour,
|
||||
Day,
|
||||
}
|
||||
|
||||
/// A duration's rounded count and unit, shared by logs and translated replies.
|
||||
pub fn duration_parts(millis: i64) -> (i64, DurationUnit) {
|
||||
let (unit, kind) = if millis <= 60 * MINUTE {
|
||||
(MINUTE, DurationUnit::Minute)
|
||||
} else if millis <= 48 * 60 * MINUTE {
|
||||
(60 * MINUTE, DurationUnit::Hour)
|
||||
} else {
|
||||
(24 * 60 * MINUTE, DurationUnit::Day)
|
||||
};
|
||||
(
|
||||
millis.div_euclid(unit) + i64::from(millis.rem_euclid(unit) > 0),
|
||||
kind,
|
||||
)
|
||||
}
|
||||
|
||||
/// `millis` for logs, rounded up: minutes up to an hour, hours up to two days, then days.
|
||||
/// `millis` for players, rounded up: minutes up to an hour, hours up to two days, then days.
|
||||
pub fn duration_text(millis: i64) -> String {
|
||||
let (count, unit) = duration_parts(millis);
|
||||
let unit = match unit {
|
||||
DurationUnit::Minute => "minute",
|
||||
DurationUnit::Hour => "hour",
|
||||
DurationUnit::Day => "day",
|
||||
let up = |unit: i64| (millis + unit - 1) / unit;
|
||||
let (count, unit) = if millis <= 60 * MINUTE {
|
||||
(up(MINUTE), "minute")
|
||||
} else if millis <= 48 * 60 * MINUTE {
|
||||
(up(60 * MINUTE), "hour")
|
||||
} else {
|
||||
(up(24 * 60 * MINUTE), "day")
|
||||
};
|
||||
format!("{count} {unit}{}", if count == 1 { "" } else { "s" })
|
||||
}
|
||||
|
||||
@@ -327,19 +327,3 @@ fn an_allow_line_without_letters_allows_nothing() {
|
||||
assert!(f.matches("nigger"), "{line}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_parts_preserve_rounding_without_overflow() {
|
||||
for (millis, expected) in [
|
||||
(1, (1, DurationUnit::Minute)),
|
||||
(60_000, (1, DurationUnit::Minute)),
|
||||
(61_000, (2, DurationUnit::Minute)),
|
||||
(3_600_000, (60, DurationUnit::Minute)),
|
||||
(3_600_001, (2, DurationUnit::Hour)),
|
||||
(172_800_000, (48, DurationUnit::Hour)),
|
||||
(172_800_001, (3, DurationUnit::Day)),
|
||||
(i64::MAX, (106_751_991_168, DurationUnit::Day)),
|
||||
] {
|
||||
assert_eq!(duration_parts(millis), expected);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ name = "horizon-client-core"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
rust-version = "1.90"
|
||||
description = "The client's lowest layer: chat text and translations, SNBT and NBT paths, the scoreboard, boss bars, game rules and the registry and particle tables commands name"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
//! NBT path parser output and byte cursor for the pinned Java oracle.
|
||||
use horizon_client_core::nbt_path::NbtPath;
|
||||
use std::io::{BufRead, Write};
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let input = std::io::BufReader::new(std::fs::File::open(args.next().unwrap())?);
|
||||
let mut output = std::io::BufWriter::new(std::fs::File::create(args.next().unwrap())?);
|
||||
for line in input.lines() {
|
||||
let text: String = serde_json::from_str(&line?)?;
|
||||
match NbtPath::parse_prefix(&text) {
|
||||
Ok((path, cursor)) => {
|
||||
write!(output, "OK {cursor} ")?;
|
||||
for value in path.as_str().as_bytes() {
|
||||
write!(output, "{value:02x}")?;
|
||||
}
|
||||
writeln!(output)?;
|
||||
}
|
||||
Err(error) => writeln!(output, "ERR {} {}", error.cursor, error.key)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -143,9 +143,7 @@ impl<'a> Fields<'a> {
|
||||
/// `RandomizableContainer.tryLoadLootTable` / `trySaveLootTable`; true when the
|
||||
/// block entity has a loot table (and so saves no items).
|
||||
fn loot_table(&mut self) -> bool {
|
||||
let Some(table) =
|
||||
text(self.input.get("LootTable")).and_then(|id| crate::identifier::normalize_id(&id))
|
||||
else {
|
||||
let Some(table) = text(self.input.get("LootTable")) else {
|
||||
return false;
|
||||
};
|
||||
self.put("LootTable", string(&table));
|
||||
@@ -217,10 +215,9 @@ pub fn stack(tag: &Tag) -> Option<Tag> {
|
||||
Some(Tag::compound(out))
|
||||
}
|
||||
/// `ItemStackTemplate.CODEC`: an item id or `{id, count?, components?}`.
|
||||
/// Encoding uses the primary record form, including for a bare item id.
|
||||
fn template(tag: &Tag) -> Option<Tag> {
|
||||
match tag {
|
||||
Tag::String(_) => Some(Tag::compound([("id", tag.clone())])),
|
||||
Tag::String(_) => Some(tag.clone()),
|
||||
Tag::Compound(_) => {
|
||||
let mut out = vec![("id", string(&text(tag.get("id"))?))];
|
||||
if let Some(count) = int(tag.get("count")).filter(|c| *c != 1) {
|
||||
@@ -908,16 +905,6 @@ mod tests {
|
||||
assert_eq!(out.get("LootTable"), Some(&string("minecraft:chests/x")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_loot_table_leaves_items_intact() {
|
||||
let out = canon(
|
||||
"{id:\"minecraft:barrel\",x:1,y:2,z:3,LootTable:\"INVALID TABLE\",\
|
||||
Items:[{Slot:0b,id:\"minecraft:stone\",count:1}]}",
|
||||
);
|
||||
assert!(out.get("Items").is_some());
|
||||
assert!(out.get("LootTable").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn items_are_validated_and_sorted_by_slot() {
|
||||
let out = canon(
|
||||
@@ -1012,69 +999,3 @@ mod tests {
|
||||
assert_eq!(items[1].get("count"), Some(&Tag::Int(5)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pot_template_tests {
|
||||
use super::*;
|
||||
use horizon_world::nbt::parse_snbt;
|
||||
|
||||
#[test]
|
||||
fn decorated_pot_string_template_load_save_uses_item_compound() {
|
||||
let parse = |s: &str| parse_snbt(s).expect("valid input NBT").0;
|
||||
let input = |front: &str| {
|
||||
let mut tag = parse(
|
||||
r#"{id:"minecraft:decorated_pot",x:4,y:200,z:4,sherds:{back:{id:"minecraft:brick"},right:{id:"minecraft:archer_pottery_sherd"}},item:{id:"minecraft:wheat",count:3,components:{"minecraft:custom_name":"H290 item"}}}"#,
|
||||
);
|
||||
let Tag::Compound(root) = &mut tag else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(Tag::Compound(sides)) = root.get_mut(&"sherds".into()) else {
|
||||
unreachable!()
|
||||
};
|
||||
sides.insert("front".into(), parse(front));
|
||||
tag
|
||||
};
|
||||
let other_fields = |loaded: &Tag, saved: &Tag| {
|
||||
assert_eq!(saved.get("id"), loaded.get("id"));
|
||||
for key in ["x", "y", "z", "item"] {
|
||||
assert_eq!(saved.get(key), loaded.get(key), "{key}");
|
||||
}
|
||||
let loaded_sides = loaded.get("sherds").unwrap();
|
||||
let saved_sides = saved.get("sherds").unwrap();
|
||||
for side in ["back", "right"] {
|
||||
assert_eq!(saved_sides.get(side), loaded_sides.get(side), "{side}");
|
||||
}
|
||||
assert_eq!(saved_sides.get("left"), None);
|
||||
};
|
||||
let plain = r#"{id:"minecraft:angler_pottery_sherd"}"#;
|
||||
let defaults = r#"{id:"minecraft:angler_pottery_sherd",count:1,components:{}}"#;
|
||||
let rich = r#"{id:"minecraft:angler_pottery_sherd",count:3,components:{"minecraft:custom_data":{h290:1},"minecraft:custom_name":"H290 sherd"}}"#;
|
||||
// Actual canonical load/save entry, not the private template helper.
|
||||
for (front, expected) in [(plain, plain), (defaults, plain), (rich, rich)] {
|
||||
let loaded = input(front);
|
||||
let original = loaded.clone();
|
||||
let saved = canonical(&loaded).expect("actual decorated-pot codec");
|
||||
other_fields(&loaded, &saved);
|
||||
assert_eq!(
|
||||
saved.get("sherds").unwrap().get("front"),
|
||||
Some(&parse(expected))
|
||||
);
|
||||
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
|
||||
assert_eq!(loaded, original, "read-only canonicalization");
|
||||
}
|
||||
println!(
|
||||
"H290 actual canonical compound/default/count/components, other sides/item/name and idempotency controls admitted"
|
||||
);
|
||||
let loaded = input(r#""minecraft:angler_pottery_sherd""#);
|
||||
let original = loaded.clone();
|
||||
let saved = canonical(&loaded).expect("actual decorated-pot codec");
|
||||
other_fields(&loaded, &saved);
|
||||
assert_eq!(loaded, original, "read-only canonicalization");
|
||||
assert_eq!(
|
||||
saved.get("sherds").unwrap().get("front"),
|
||||
Some(&parse(plain)),
|
||||
"string template must save the primary item compound",
|
||||
);
|
||||
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
|
||||
}
|
||||
}
|
||||
@@ -111,15 +111,7 @@ impl Parser<'_> {
|
||||
map.insert(key, self.value()?);
|
||||
self.ws();
|
||||
match self.peek() {
|
||||
// SnbtGrammar: an entry may be followed by a comma before the brace.
|
||||
Some(b',') => {
|
||||
self.i += 1;
|
||||
self.ws();
|
||||
if self.peek() == Some(b'}') {
|
||||
self.i += 1;
|
||||
return Ok(Value::Object(map));
|
||||
}
|
||||
}
|
||||
Some(b',') => self.i += 1,
|
||||
Some(b'}') => {
|
||||
self.i += 1;
|
||||
return Ok(Value::Object(map));
|
||||
@@ -148,15 +140,7 @@ impl Parser<'_> {
|
||||
list.push(self.value()?);
|
||||
self.ws();
|
||||
match self.peek() {
|
||||
// SnbtGrammar: a list or array may end with a comma.
|
||||
Some(b',') => {
|
||||
self.i += 1;
|
||||
self.ws();
|
||||
if self.peek() == Some(b']') {
|
||||
self.i += 1;
|
||||
return Ok(Value::Array(list));
|
||||
}
|
||||
}
|
||||
Some(b',') => self.i += 1,
|
||||
Some(b']') => {
|
||||
self.i += 1;
|
||||
return Ok(Value::Array(list));
|
||||
@@ -223,11 +207,9 @@ fn style_flag(v: Option<&Value>) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
/// ExtraCodecs.UNTRUSTED_URI / Util.parseAndValidateUntrustedUri: http and
|
||||
/// https only, scheme case-insensitive. Every `open_url` a server sends must
|
||||
/// pass it: anything else (`file:`, a UNC path, a custom protocol handler)
|
||||
/// could start a program through the system's URL handler.
|
||||
pub fn untrusted_http_url(url: &str) -> bool {
|
||||
fn untrusted_http_url(url: &str) -> bool {
|
||||
// ExtraCodecs.UNTRUSTED_URI / Util.parseAndValidateUntrustedUri: http and
|
||||
// https only, scheme case-insensitive.
|
||||
url.split_once(':').is_some_and(|(scheme, _)| {
|
||||
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
|
||||
})
|
||||
@@ -412,11 +394,10 @@ pub fn to_text_with(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// TranslatableContents.decompose: Language.getOrDefault(key, fallback), so the
|
||||
// fallback is the template itself, never looked up as another key.
|
||||
let translated = Text::translate(key, &args);
|
||||
match o.get("fallback").and_then(Value::as_str) {
|
||||
Some(fallback) => Text::translate_with_fallback(key, fallback, &args),
|
||||
None => Text::translate(key, &args),
|
||||
Some(fallback) if translated.plain() == key => Text::translate(fallback, &args),
|
||||
_ => translated,
|
||||
}
|
||||
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
|
||||
resolve(sel)
|
||||
@@ -514,59 +495,6 @@ pub fn name_text(tag: &Tag) -> Text {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lang::{self, Language, ResourceSource};
|
||||
|
||||
struct Mem(std::collections::HashMap<String, Vec<u8>>);
|
||||
impl ResourceSource for Mem {
|
||||
fn read(&self, path: &str) -> Option<Vec<u8>> {
|
||||
self.0.get(path).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// TranslatableContents.decompose: Language.getOrDefault(key, fallback). A missing key
|
||||
/// shows the fallback as written, even when the fallback is another key the language has.
|
||||
#[test]
|
||||
fn a_translatable_fallback_stays_literal() {
|
||||
let pack = Mem([(
|
||||
"assets/minecraft/lang/en_us.json".to_owned(),
|
||||
br#"{"real.key": "Translated", "greet": "Hello %s"}"#.to_vec(),
|
||||
)]
|
||||
.into());
|
||||
let ((), _) = lang::scoped(Language::load(&[&pack], "en_us"), || {
|
||||
let parse = |v: Value| to_text(&v, &mut |_| Text::empty()).plain();
|
||||
let missing = serde_json::json!({"translate": "missing.key", "fallback": "real.key"});
|
||||
assert_eq!(parse(missing), "real.key");
|
||||
// The fallback is a template: its arguments are filled in.
|
||||
let args = serde_json::json!({"translate": "missing.key", "fallback": "Hi %s", "with": ["Steve"]});
|
||||
assert_eq!(parse(args), "Hi Steve");
|
||||
// A key the language has wins over its fallback.
|
||||
let found =
|
||||
serde_json::json!({"translate": "greet", "fallback": "real.key", "with": ["Alex"]});
|
||||
assert_eq!(parse(found), "Hello Alex");
|
||||
// A component built with the constructor keeps its meaning through its JSON.
|
||||
let built = Text::translate_with_fallback("missing.key", "real.key", &[]);
|
||||
let again = to_text(&built.component(), &mut |_| Text::empty());
|
||||
assert_eq!(again.plain(), built.plain());
|
||||
assert_eq!(again.component(), built.component());
|
||||
});
|
||||
}
|
||||
|
||||
/// SnbtGrammar: a compound or list may end with one comma; two in a row
|
||||
/// stay an error.
|
||||
#[test]
|
||||
fn a_component_takes_one_trailing_comma() {
|
||||
let parse = |s: &str| parse_snbt(s).map(|(v, _)| v);
|
||||
assert_eq!(
|
||||
parse("{text:\"a\",color:\"red\",}"),
|
||||
parse("{text:\"a\",color:\"red\"}")
|
||||
);
|
||||
assert_eq!(
|
||||
parse("[\"a\", {text:\"b\",} ,]"),
|
||||
parse("[\"a\",{text:\"b\"}]")
|
||||
);
|
||||
assert!(parse("{text:\"a\",,}").is_err());
|
||||
assert!(parse("[\"a\",,]").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_color_follows_text_color_parse_color() {
|
||||
|
||||
@@ -46,21 +46,6 @@ thread_local! {
|
||||
}
|
||||
/// The current language's `bidirectional`, read for every drawn string.
|
||||
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
|
||||
/// Counts the languages set (`set`): another count is another language, so text looked up
|
||||
/// before it may read otherwise.
|
||||
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
thread_local! {
|
||||
/// Counts the scopes this thread entered and left (`scoped`), which no other thread sees.
|
||||
static SCOPE_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
/// Which language text looks up in now, on this thread, as a count that changes whenever that
|
||||
/// can: both counts only grow, so their sum changes when either does.
|
||||
pub fn generation() -> u64 {
|
||||
GENERATION.load(Ordering::Relaxed) + SCOPE_GENERATION.get()
|
||||
}
|
||||
fn scope_changed() {
|
||||
SCOPE_GENERATION.set(SCOPE_GENERATION.get() + 1);
|
||||
}
|
||||
|
||||
/// A source of resource files: the prepared root plus packs, highest priority last.
|
||||
pub trait ResourceSource {
|
||||
@@ -115,222 +100,6 @@ impl ResourceSource for Bundled {
|
||||
"assets/horizon/lang/ur_pk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ur_pk.json")
|
||||
}
|
||||
"assets/horizon/lang/fa_ir.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fa_ir.json")
|
||||
}
|
||||
"assets/horizon/lang/he_il.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/he_il.json")
|
||||
}
|
||||
"assets/horizon/lang/hu_hu.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/hu_hu.json")
|
||||
}
|
||||
"assets/horizon/lang/ro_ro.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ro_ro.json")
|
||||
}
|
||||
"assets/horizon/lang/da_dk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/da_dk.json")
|
||||
}
|
||||
"assets/horizon/lang/fi_fi.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fi_fi.json")
|
||||
}
|
||||
"assets/horizon/lang/no_no.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/no_no.json")
|
||||
}
|
||||
"assets/horizon/lang/bg_bg.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/bg_bg.json")
|
||||
}
|
||||
"assets/horizon/lang/sk_sk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sk_sk.json")
|
||||
}
|
||||
"assets/horizon/lang/lt_lt.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lt_lt.json")
|
||||
}
|
||||
"assets/horizon/lang/it_it.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/it_it.json")
|
||||
}
|
||||
"assets/horizon/lang/nl_nl.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/nl_nl.json")
|
||||
}
|
||||
"assets/horizon/lang/vi_vn.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/vi_vn.json")
|
||||
}
|
||||
"assets/horizon/lang/th_th.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/th_th.json")
|
||||
}
|
||||
"assets/horizon/lang/ja_jp.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ja_jp.json")
|
||||
}
|
||||
"assets/horizon/lang/ar_sa.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ar_sa.json")
|
||||
}
|
||||
"assets/horizon/lang/pt_br.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/pt_br.json")
|
||||
}
|
||||
"assets/horizon/lang/cs_cz.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/cs_cz.json")
|
||||
}
|
||||
"assets/horizon/lang/sv_se.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sv_se.json")
|
||||
}
|
||||
"assets/horizon/lang/el_gr.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/el_gr.json")
|
||||
}
|
||||
"assets/horizon/lang/ca_es.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ca_es.json")
|
||||
}
|
||||
"assets/horizon/lang/et_ee.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/et_ee.json")
|
||||
}
|
||||
"assets/horizon/lang/lv_lv.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lv_lv.json")
|
||||
}
|
||||
"assets/horizon/lang/sl_si.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sl_si.json")
|
||||
}
|
||||
"assets/horizon/lang/sr_cs.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sr_cs.json")
|
||||
}
|
||||
"assets/horizon/lang/is_is.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/is_is.json")
|
||||
}
|
||||
"assets/horizon/lang/af_za.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/af_za.json")
|
||||
}
|
||||
"assets/horizon/lang/ms_my.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ms_my.json")
|
||||
}
|
||||
"assets/horizon/lang/be_by.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/be_by.json")
|
||||
}
|
||||
"assets/horizon/lang/sr_sp.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sr_sp.json")
|
||||
}
|
||||
"assets/horizon/lang/az_az.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/az_az.json")
|
||||
}
|
||||
"assets/horizon/lang/uz_uz.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/uz_uz.json")
|
||||
}
|
||||
"assets/horizon/lang/kk_kz.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/kk_kz.json")
|
||||
}
|
||||
"assets/horizon/lang/ka_ge.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ka_ge.json")
|
||||
}
|
||||
"assets/horizon/lang/hy_am.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/hy_am.json")
|
||||
}
|
||||
"assets/horizon/lang/bs_ba.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/bs_ba.json")
|
||||
}
|
||||
"assets/horizon/lang/mk_mk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/mk_mk.json")
|
||||
}
|
||||
"assets/horizon/lang/sq_al.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sq_al.json")
|
||||
}
|
||||
"assets/horizon/lang/cy_gb.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/cy_gb.json")
|
||||
}
|
||||
"assets/horizon/lang/ga_ie.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ga_ie.json")
|
||||
}
|
||||
"assets/horizon/lang/eu_es.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/eu_es.json")
|
||||
}
|
||||
"assets/horizon/lang/gl_es.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/gl_es.json")
|
||||
}
|
||||
"assets/horizon/lang/fil_ph.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fil_ph.json")
|
||||
}
|
||||
"assets/horizon/lang/ta_in.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ta_in.json")
|
||||
}
|
||||
"assets/horizon/lang/mn_mn.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/mn_mn.json")
|
||||
}
|
||||
"assets/horizon/lang/lo_la.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lo_la.json")
|
||||
}
|
||||
"assets/horizon/lang/so_so.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/so_so.json")
|
||||
}
|
||||
"assets/horizon/lang/fy_nl.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fy_nl.json")
|
||||
}
|
||||
"assets/horizon/lang/gd_gb.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/gd_gb.json")
|
||||
}
|
||||
"assets/horizon/lang/lb_lu.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lb_lu.json")
|
||||
}
|
||||
"assets/horizon/lang/ky_kg.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ky_kg.json")
|
||||
}
|
||||
"assets/horizon/lang/kn_in.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/kn_in.json")
|
||||
}
|
||||
"assets/horizon/lang/mt_mt.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/mt_mt.json")
|
||||
}
|
||||
"assets/horizon/lang/eo_uy.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/eo_uy.json")
|
||||
}
|
||||
"assets/horizon/lang/haw_us.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/haw_us.json")
|
||||
}
|
||||
"assets/horizon/lang/ig_ng.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ig_ng.json")
|
||||
}
|
||||
"assets/horizon/lang/la_la.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/la_la.json")
|
||||
}
|
||||
"assets/horizon/lang/tt_ru.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/tt_ru.json")
|
||||
}
|
||||
"assets/horizon/lang/yi_de.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/yi_de.json")
|
||||
}
|
||||
"assets/horizon/lang/yo_ng.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/yo_ng.json")
|
||||
}
|
||||
"assets/horizon/lang/ba_ru.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ba_ru.json")
|
||||
}
|
||||
"assets/horizon/lang/br_fr.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/br_fr.json")
|
||||
}
|
||||
"assets/horizon/lang/cv_cu.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/cv_cu.json")
|
||||
}
|
||||
"assets/horizon/lang/fo_fo.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fo_fo.json")
|
||||
}
|
||||
"assets/horizon/lang/fur_it.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fur_it.json")
|
||||
}
|
||||
"assets/horizon/lang/li_li.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/li_li.json")
|
||||
}
|
||||
"assets/horizon/lang/lmo.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lmo.json")
|
||||
}
|
||||
"assets/horizon/lang/oc_fr.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/oc_fr.json")
|
||||
}
|
||||
"assets/horizon/lang/sah_sah.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sah_sah.json")
|
||||
}
|
||||
"assets/horizon/lang/se_no.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/se_no.json")
|
||||
}
|
||||
"assets/horizon/lang/szl.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/szl.json")
|
||||
}
|
||||
"assets/horizon/lang/vec_it.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/vec_it.json")
|
||||
}
|
||||
"assets/horizon/lang/de_de.json" | "assets/horizon/lang/de_at.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/de_de.json")
|
||||
}
|
||||
@@ -403,36 +172,6 @@ pub fn apply_deprecated(map: &mut HashMap<String, String>, json: &[u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Language.loadFromJson` for one `lang/<code>.json`: its entries go into `map` in file order.
|
||||
/// A file that is plain JSON reads as before; any other goes through Gson's lenient reader
|
||||
/// (`#`, `//` and `/* */` comments, unquoted or single-quoted names and values, raw control
|
||||
/// characters in strings, `=` or `=>` and `;`), which vanilla reads lang files with (expected
|
||||
/// from `Language.loadFromJson`, Gson). A file that is not JSON even so adds nothing; a value
|
||||
/// that is not a string, number or boolean (`GsonHelper.convertToString`) ends the file there.
|
||||
pub fn read_lang_file(bytes: &[u8], map: &mut HashMap<String, String>) {
|
||||
use horizon_world::datafix::json::{Json, parse_lenient};
|
||||
if let Ok(entries) = serde_json::from_slice::<HashMap<String, String>>(bytes) {
|
||||
map.extend(entries);
|
||||
return;
|
||||
}
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
// Gson's JsonReader skips a leading byte order mark.
|
||||
let text = text.strip_prefix('\u{feff}').unwrap_or(&text);
|
||||
let utf16: Vec<u16> = text.encode_utf16().collect();
|
||||
let Some(Json::Obj(entries)) = parse_lenient(&utf16) else {
|
||||
return;
|
||||
};
|
||||
for (key, value) in entries {
|
||||
let value = match value {
|
||||
Json::Str(s) => String::from_utf16_lossy(&s),
|
||||
Json::Num(n) => n,
|
||||
Json::Bool(b) => b.to_string(),
|
||||
Json::Null | Json::Arr(_) | Json::Obj(_) => break,
|
||||
};
|
||||
map.insert(String::from_utf16_lossy(&key), value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Language {
|
||||
/// ClientLanguage.loadFrom: en_us first, then the selected language, each
|
||||
/// from every namespace and every source in priority order (later sources
|
||||
@@ -458,7 +197,9 @@ impl Language {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
read_lang_file(&bytes, &mut map);
|
||||
if let Ok(entries) = serde_json::from_slice::<HashMap<String, String>>(&bytes) {
|
||||
map.extend(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +280,6 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
|
||||
pub fn set(language: Language) {
|
||||
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
|
||||
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
|
||||
GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
pub fn current() -> Option<Arc<Language>> {
|
||||
if let Some(language) = SCOPE.with(|s| s.borrow().as_ref().map(|s| s.language.clone())) {
|
||||
@@ -559,7 +299,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
SCOPE.with(|s| *s.borrow_mut() = self.0.take());
|
||||
scope_changed();
|
||||
}
|
||||
}
|
||||
let outer = SCOPE.with(|s| {
|
||||
@@ -569,7 +308,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
|
||||
})
|
||||
});
|
||||
let _restore = Restore(outer);
|
||||
scope_changed();
|
||||
let result = f();
|
||||
let missed = SCOPE.with(|s| {
|
||||
s.borrow_mut()
|
||||
@@ -921,165 +659,4 @@ mod tests {
|
||||
["en_us", "fr_fr"]
|
||||
);
|
||||
}
|
||||
|
||||
fn read(body: &str) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
read_lang_file(body.as_bytes(), &mut map);
|
||||
map
|
||||
}
|
||||
fn entries(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plain_json_lang_file_reads_as_before() {
|
||||
let body = "{\"a\": \"Apple \\\"quoted\\\" \\u00e9\", \"b\": \"\", \"a\": \"Again\"}";
|
||||
let strict: HashMap<String, String> = serde_json::from_str(body).unwrap();
|
||||
assert_eq!(read(body), strict);
|
||||
assert_eq!(read(body), entries(&[("a", "Again"), ("b", "")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lang_files_read_with_comments() {
|
||||
let want = entries(&[("a", "Apple"), ("b", "Banana # not a comment // either")]);
|
||||
for body in [
|
||||
"# header\n{\n# a comment\n\"a\": \"Apple\",\n\"b\": \"Banana # not a comment // either\"\n}\n",
|
||||
"// header\n{\"a\": \"Apple\", // after\n\"b\": \"Banana # not a comment // either\"}",
|
||||
"/* header\n over lines */{\"a\": /* inline */ \"Apple\", \"b\": \"Banana # not a comment // either\"}",
|
||||
] {
|
||||
assert_eq!(read(body), want, "{body}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lang_files_read_unquoted_and_single_quoted_names_and_values() {
|
||||
assert_eq!(
|
||||
read("{a: 'Apple', 'b': \"Banana\", c: Cherry}"),
|
||||
entries(&[("a", "Apple"), ("b", "Banana"), ("c", "Cherry")])
|
||||
);
|
||||
// A single-quoted string may hold a double quote, and the reverse.
|
||||
assert_eq!(
|
||||
read("{'a': 'say \"hi\"', \"b\": \"it's\"}"),
|
||||
entries(&[("a", "say \"hi\""), ("b", "it's")])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lang_files_read_raw_control_characters_in_strings() {
|
||||
assert_eq!(
|
||||
read("{\"a\": \"tab\there\", \"b\": \"two\nlines\"}"),
|
||||
entries(&[("a", "tab\there"), ("b", "two\nlines")])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lang_files_read_gson_separators_and_a_byte_order_mark() {
|
||||
assert_eq!(
|
||||
read("\u{feff}{\"a\" = \"Apple\"; \"b\" => \"Banana\"}"),
|
||||
entries(&[("a", "Apple"), ("b", "Banana")])
|
||||
);
|
||||
// A trailing comma before `}` is not JSON even to Gson's lenient reader.
|
||||
assert_eq!(read("{# c\n\"a\": \"Apple\",}"), HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_and_booleans_read_as_their_text() {
|
||||
// GsonHelper.convertToString: a primitive reads as its text.
|
||||
assert_eq!(
|
||||
read("{\"a\": 1.50, \"b\": true, \"c\": -2e3}"),
|
||||
entries(&[("a", "1.50"), ("b", "true"), ("c", "-2e3")])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_value_that_is_no_text_ends_the_file_there() {
|
||||
assert_eq!(
|
||||
read("# c\n{\"a\": \"Apple\", \"b\": [\"x\"], \"c\": \"Cherry\"}"),
|
||||
entries(&[("a", "Apple")])
|
||||
);
|
||||
assert_eq!(read("{\"a\": null, \"b\": \"Banana\"}"), HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lang_file_that_is_not_json_adds_nothing() {
|
||||
for body in [
|
||||
"# c\n{\"a\": \"Apple\"",
|
||||
"[\"a\"]",
|
||||
"\"a\"",
|
||||
"",
|
||||
"{\"a\": \"Apple\"} {",
|
||||
] {
|
||||
assert_eq!(read(body), HashMap::new(), "{body:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pack_lang_file_with_comments_layers_over_english() {
|
||||
let base = Mem(HashMap::from([(
|
||||
"assets/minecraft/lang/en_us.json".into(),
|
||||
br#"{"a":"Apple","b":"Banana"}"#.to_vec(),
|
||||
)]));
|
||||
let pack = Mem(HashMap::from([(
|
||||
"assets/minecraft/lang/fr_fr.json".into(),
|
||||
b"{\n# Fruits\n\"a\": \"Pomme\"\n}\n".to_vec(),
|
||||
)]));
|
||||
let sources: [&dyn ResourceSource; 2] = [&base, &pack];
|
||||
let fr = Language::load(&sources, "fr_fr");
|
||||
assert_eq!(fr.get("a"), Some("Pomme"));
|
||||
assert_eq!(fr.get("b"), Some("Banana"));
|
||||
}
|
||||
|
||||
/// Every lang file of a downloaded pack, e.g. BACAP Language Pack:
|
||||
/// `HORIZON_LANG_PACK_DIR=<pack>/assets/minecraft/lang cargo test -p horizon-client-core
|
||||
/// lang::tests::every_lang_file_of_a_pack_reads -- --ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore = "needs a downloaded pack in HORIZON_LANG_PACK_DIR"]
|
||||
fn every_lang_file_of_a_pack_reads() {
|
||||
let Ok(dir) = std::env::var("HORIZON_LANG_PACK_DIR") else {
|
||||
return;
|
||||
};
|
||||
let mut failed = Vec::new();
|
||||
let mut paths: Vec<_> = std::fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().path())
|
||||
.collect();
|
||||
paths.sort();
|
||||
for path in paths {
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let strict = serde_json::from_slice::<HashMap<String, String>>(&bytes).is_ok();
|
||||
let mut map = HashMap::new();
|
||||
read_lang_file(&bytes, &mut map);
|
||||
// An independent count: the distinct `"name":` that start a line.
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
let names: std::collections::HashSet<&str> = text
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let rest = line.trim_start().strip_prefix('"')?;
|
||||
let mut escaped = false;
|
||||
let end = rest.char_indices().find_map(|(i, c)| {
|
||||
let close = c == '"' && !escaped;
|
||||
escaped = c == '\\' && !escaped;
|
||||
close.then_some(i)
|
||||
})?;
|
||||
rest[end + 1..]
|
||||
.trim_start()
|
||||
.starts_with(':')
|
||||
.then(|| &rest[..end])
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
"{}: strict {strict}, {} keys, {} names",
|
||||
path.display(),
|
||||
map.len(),
|
||||
names.len()
|
||||
);
|
||||
if map.is_empty() || map.len() != names.len() {
|
||||
failed.push(path);
|
||||
}
|
||||
}
|
||||
assert!(failed.is_empty(), "{failed:?}");
|
||||
}
|
||||
}
|
||||
@@ -79,65 +79,28 @@ fn elements(tag: &Tag) -> Option<Vec<Tag>> {
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// `DoubleTag.intValue` / `FloatTag.intValue` floor before the i32 conversion;
|
||||
/// integral tags use their intValue narrowing. Byte arrays then keep the low byte.
|
||||
pub fn int_value(t: &Tag) -> Option<i32> {
|
||||
Some(match t {
|
||||
Tag::Byte(v) => i32::from(*v),
|
||||
Tag::Short(v) => i32::from(*v),
|
||||
Tag::Int(v) => *v,
|
||||
Tag::Long(v) => *v as i32,
|
||||
Tag::Float(v) => f32::from_bits(*v).floor() as i32,
|
||||
Tag::Double(v) => f64::from_bits(*v).floor() as i32,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// `FloatTag.longValue` truncates; `DoubleTag.longValue` floors before narrowing.
|
||||
fn long_value(t: &Tag) -> Option<i64> {
|
||||
Some(match t {
|
||||
Tag::Byte(v) => i64::from(*v),
|
||||
Tag::Short(v) => i64::from(*v),
|
||||
Tag::Int(v) => i64::from(*v),
|
||||
Tag::Long(v) => *v,
|
||||
// FloatTag.longValue truncates; DoubleTag.longValue floors first.
|
||||
Tag::Float(v) => f32::from_bits(*v) as i64,
|
||||
Tag::Double(v) => f64::from_bits(*v).floor() as i64,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
enum ElementKind {
|
||||
List,
|
||||
NumericArray,
|
||||
}
|
||||
impl ElementKind {
|
||||
// Admission only: arrays require NumericTag; lists accept every tag type.
|
||||
fn accepts(self, value: &Tag) -> bool {
|
||||
matches!(self, Self::List)
|
||||
|| matches!(
|
||||
value,
|
||||
Tag::Byte(_)
|
||||
| Tag::Short(_)
|
||||
| Tag::Int(_)
|
||||
| Tag::Long(_)
|
||||
| Tag::Float(_)
|
||||
| Tag::Double(_)
|
||||
)
|
||||
fn number(t: &Tag) -> Option<i64> {
|
||||
match t {
|
||||
Tag::Byte(v) => Some(i64::from(*v)),
|
||||
Tag::Short(v) => Some(i64::from(*v)),
|
||||
Tag::Int(v) => Some(i64::from(*v)),
|
||||
Tag::Long(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Rust view adapter: run `f` over the elements, then write arrays using their numeric accessor.
|
||||
/// Run `f` over the elements of a list/array, writing array results back.
|
||||
/// None when `tag` is not list-like.
|
||||
fn with_elements<R>(tag: &mut Tag, f: impl FnOnce(&mut Vec<Tag>, ElementKind) -> R) -> Option<R> {
|
||||
fn with_elements<R>(tag: &mut Tag, f: impl FnOnce(&mut Vec<Tag>) -> R) -> Option<R> {
|
||||
match tag {
|
||||
Tag::List(l) => Some(f(l, ElementKind::List)),
|
||||
Tag::List(l) => Some(f(l)),
|
||||
Tag::ByteArray(_) | Tag::IntArray(_) | Tag::LongArray(_) => {
|
||||
let mut v = elements(tag)?;
|
||||
let r = f(&mut v, ElementKind::NumericArray);
|
||||
let r = f(&mut v);
|
||||
let nums: Vec<i64> = v.iter().filter_map(number).collect();
|
||||
match tag {
|
||||
// Float/double byteValue uses intValue's floor, then narrows.
|
||||
Tag::ByteArray(a) => *a = v.iter().filter_map(int_value).map(|n| n as u8).collect(),
|
||||
Tag::IntArray(a) => *a = v.iter().filter_map(int_value).collect(),
|
||||
Tag::LongArray(a) => *a = v.iter().filter_map(long_value).collect(),
|
||||
Tag::ByteArray(a) => *a = nums.iter().map(|n| *n as u8).collect(),
|
||||
Tag::IntArray(a) => *a = nums.iter().map(|n| *n as i32).collect(),
|
||||
Tag::LongArray(a) => *a = nums,
|
||||
_ => {}
|
||||
}
|
||||
Some(r)
|
||||
@@ -194,20 +157,6 @@ impl NbtPath {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.nodes.is_empty()
|
||||
}
|
||||
/// Whether this selects a compound-field chain, including filtered or quoted fields.
|
||||
/// Matching a root or child does not change that subtree's writer provenance.
|
||||
pub fn is_field_path(&self, fields: &[&str]) -> bool {
|
||||
let mut fields = fields.iter();
|
||||
for node in &self.nodes {
|
||||
match node {
|
||||
Node::MatchRoot(_) => {}
|
||||
Node::Child(name) | Node::MatchChild(name, _)
|
||||
if fields.next().copied() == Some(name.as_str()) => {}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
fields.next().is_none()
|
||||
}
|
||||
fn not_found(&self, node: usize) -> PathError {
|
||||
let end = self.ends[node];
|
||||
PathError::new(
|
||||
@@ -400,7 +349,7 @@ impl NbtPath {
|
||||
});
|
||||
removed
|
||||
}
|
||||
/// `NbtPathArgument.NbtPath.insert`: insert accepted tags into every reached collection.
|
||||
/// `insert` (also append and prepend): into every list the path reaches.
|
||||
pub fn insert(
|
||||
&self,
|
||||
index: i32,
|
||||
@@ -423,23 +372,19 @@ impl NbtPath {
|
||||
return;
|
||||
}
|
||||
let shown = crate::snbt::to_snbt(target);
|
||||
let result = with_elements(target, |list, kind| {
|
||||
let result = with_elements(target, |list| {
|
||||
let start = if index < 0 {
|
||||
list.len() as i64 + i64::from(index) + 1
|
||||
} else {
|
||||
i64::from(index)
|
||||
};
|
||||
let mut any = false;
|
||||
let mut at = start;
|
||||
for v in values {
|
||||
if !kind.accepts(v) {
|
||||
continue;
|
||||
}
|
||||
for (n, v) in values.iter().enumerate() {
|
||||
let at = start + n as i64;
|
||||
if at < 0 || at as usize > list.len() {
|
||||
return Err(CollectionError::InvalidIndex(at as i32));
|
||||
}
|
||||
list.insert(at as usize, v.clone());
|
||||
at += 1;
|
||||
any = true;
|
||||
}
|
||||
Ok(any)
|
||||
@@ -483,8 +428,7 @@ fn preferred_parent(node: &Node) -> Tag {
|
||||
_ => Tag::Compound(Compound::new()),
|
||||
}
|
||||
}
|
||||
/// Children of `parent`; the array creation arm follows
|
||||
/// `NbtPathArgument.AllElementsNode.getOrCreateTag` admission.
|
||||
/// `getOrCreateTag` (or `getTag` without `create`): the children of `parent`.
|
||||
fn step_mut(
|
||||
node: &Node,
|
||||
parent: &mut Tag,
|
||||
@@ -524,14 +468,11 @@ fn step_mut(
|
||||
}
|
||||
Node::AllElements => {
|
||||
let make = create;
|
||||
let _ = with_elements(parent, |list, kind| {
|
||||
let _ = with_elements(parent, |list| {
|
||||
if list.is_empty() {
|
||||
if let Some(make) = make {
|
||||
let value = make();
|
||||
if kind.accepts(&value) {
|
||||
list.push(value);
|
||||
out(&mut list[0]);
|
||||
}
|
||||
list.push(make());
|
||||
out(&mut list[0]);
|
||||
}
|
||||
} else {
|
||||
for t in list.iter_mut() {
|
||||
@@ -541,7 +482,7 @@ fn step_mut(
|
||||
});
|
||||
}
|
||||
Node::Index(i) => {
|
||||
let _ = with_elements(parent, |list, _kind| {
|
||||
let _ = with_elements(parent, |list| {
|
||||
let n = list.len() as i64;
|
||||
let idx = if *i < 0 {
|
||||
n + i64::from(*i)
|
||||
@@ -577,8 +518,7 @@ fn step_mut(
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Element setters follow `NbtPathArgument.AllElementsNode.setTag` and
|
||||
/// `NbtPathArgument.IndexedElementNode.setTag`; object nodes replace their children.
|
||||
/// `Node.setTag`: replace the child(ren) of `parent` with `value`.
|
||||
fn set_tag(node: &Node, parent: &mut Tag, value: &Tag) -> usize {
|
||||
match node {
|
||||
Node::Child(name) => {
|
||||
@@ -598,36 +538,29 @@ fn set_tag(node: &Node, parent: &mut Tag, value: &Tag) -> usize {
|
||||
}
|
||||
0
|
||||
}
|
||||
Node::AllElements => with_elements(parent, |list, kind| {
|
||||
let len = list.len();
|
||||
if len == 0 {
|
||||
if kind.accepts(value) {
|
||||
list.push(value.clone());
|
||||
}
|
||||
// AllElementsNode returns 1 here even when addTag rejects.
|
||||
Node::AllElements => with_elements(parent, |list| {
|
||||
if list.is_empty() {
|
||||
list.push(value.clone());
|
||||
return 1;
|
||||
}
|
||||
// Compare the original tags, before numeric conversion.
|
||||
let changed = list.iter().filter(|t| *t != value).count();
|
||||
if changed == 0 {
|
||||
return 0;
|
||||
}
|
||||
list.clear();
|
||||
if !kind.accepts(value) {
|
||||
return 0;
|
||||
for t in list.iter_mut() {
|
||||
*t = value.clone();
|
||||
}
|
||||
list.resize(len, value.clone());
|
||||
changed
|
||||
})
|
||||
.unwrap_or(0),
|
||||
Node::Index(i) => with_elements(parent, |list, kind| {
|
||||
Node::Index(i) => with_elements(parent, |list| {
|
||||
let n = list.len() as i64;
|
||||
let idx = if *i < 0 {
|
||||
n + i64::from(*i)
|
||||
} else {
|
||||
i64::from(*i)
|
||||
};
|
||||
if (0..n).contains(&idx) && list[idx as usize] != *value && kind.accepts(value) {
|
||||
if (0..n).contains(&idx) && list[idx as usize] != *value {
|
||||
list[idx as usize] = value.clone();
|
||||
1
|
||||
} else {
|
||||
@@ -653,7 +586,6 @@ fn set_tag(node: &Node, parent: &mut Tag, value: &Tag) -> usize {
|
||||
Node::MatchRoot(_) => 0,
|
||||
}
|
||||
}
|
||||
/// Removal arms, including `NbtPathArgument.AllElementsNode.removeTag`.
|
||||
fn remove_tag(node: &Node, parent: &mut Tag) -> usize {
|
||||
match node {
|
||||
Node::Child(name) => match parent {
|
||||
@@ -671,13 +603,13 @@ fn remove_tag(node: &Node, parent: &mut Tag) -> usize {
|
||||
}
|
||||
_ => 0,
|
||||
},
|
||||
Node::AllElements => with_elements(parent, |l, _kind| {
|
||||
Node::AllElements => with_elements(parent, |l| {
|
||||
let n = l.len();
|
||||
l.clear();
|
||||
n
|
||||
})
|
||||
.unwrap_or(0),
|
||||
Node::Index(i) => with_elements(parent, |l, _kind| {
|
||||
Node::Index(i) => with_elements(parent, |l| {
|
||||
let n = l.len() as i64;
|
||||
let idx = if *i < 0 {
|
||||
n + i64::from(*i)
|
||||
@@ -726,16 +658,11 @@ fn parse_node(text: &str, i: usize, first: bool) -> Result<(Node, usize), PathEr
|
||||
Some(b']') => Ok((Node::AllElements, j + 1)),
|
||||
_ => {
|
||||
let start = j;
|
||||
// StringReader.readInt scans all allowed number characters
|
||||
// before parsing, including malformed signs and decimal points.
|
||||
while b
|
||||
.get(j)
|
||||
.is_some_and(|c| c.is_ascii_digit() || matches!(c, b'-' | b'.'))
|
||||
{
|
||||
if b.get(j) == Some(&b'-') {
|
||||
j += 1;
|
||||
}
|
||||
if start == j {
|
||||
return Err(PathError::new("parsing.int.expected", vec![], start));
|
||||
while b.get(j).is_some_and(u8::is_ascii_digit) {
|
||||
j += 1;
|
||||
}
|
||||
let n: i32 = text[start..j].parse().map_err(|_| {
|
||||
PathError::new(
|
||||
@@ -797,13 +724,6 @@ fn read_quoted(text: &str, i: usize) -> Result<(String, usize), PathError> {
|
||||
let mut escaped = false;
|
||||
for (o, c) in text[i + 1..].char_indices() {
|
||||
if escaped {
|
||||
if c != quote && c != '\\' {
|
||||
return Err(PathError::new(
|
||||
"parsing.quote.escape",
|
||||
vec![c.to_string()],
|
||||
i + 1 + o,
|
||||
));
|
||||
}
|
||||
out.push(c);
|
||||
escaped = false;
|
||||
} else if c == '\\' {
|
||||
@@ -852,59 +772,6 @@ fn parse_compound(text: &str, i: usize) -> Result<(Tag, usize), PathError> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn quoted_escapes_and_index_tokens_match_brigadier() {
|
||||
let cases = [
|
||||
(r#""a\q""#, "parsing.quote.escape", vec!["q"], 3),
|
||||
(r#"'a\x'"#, "parsing.quote.escape", vec!["x"], 3),
|
||||
("[1.0]", "parsing.int.invalid", vec!["1.0"], 1),
|
||||
("[.]", "parsing.int.invalid", vec!["."], 1),
|
||||
("[--1]", "parsing.int.invalid", vec!["--1"], 1),
|
||||
("[2147483648]", "parsing.int.invalid", vec!["2147483648"], 1),
|
||||
("[+1]", "parsing.int.expected", vec![], 1),
|
||||
(r#""é\q""#, "parsing.quote.escape", vec!["q"], 4),
|
||||
(r#""é"[1.0]"#, "parsing.int.invalid", vec!["1.0"], 5),
|
||||
];
|
||||
let mut failures = Vec::new();
|
||||
for (text, key, args, cursor) in cases {
|
||||
match NbtPath::parse_prefix(text) {
|
||||
Err(error) if error.key == key && error.args == args && error.cursor == cursor => {}
|
||||
other => failures.push(format!("{text:?}: {other:?}")),
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_quoted_escapes_still_select_the_exact_field() {
|
||||
let root = Tag::compound([("a\"b", Tag::Int(7)), ("a\\b", Tag::Int(8))]);
|
||||
for (path, value) in [(r#""a\"b""#, 7), (r#""a\\b""#, 8)] {
|
||||
assert_eq!(
|
||||
NbtPath::parse(path).unwrap().get(&root).unwrap(),
|
||||
vec![Tag::Int(value)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_provenance_handles_quotes_and_filters_without_matching_lists() {
|
||||
for text in [
|
||||
"blockState.properties",
|
||||
"\"blockState\".\"properties\"",
|
||||
"{source:1b}.blockState.properties",
|
||||
"blockState{id:\"minecraft:piston_head\"}.properties",
|
||||
] {
|
||||
let path = NbtPath::parse(text).unwrap();
|
||||
assert!(path.is_field_path(&["blockState", "properties"]));
|
||||
assert!(!path.is_field_path(&["blockState"]));
|
||||
}
|
||||
assert!(NbtPath::parse("{source:1b}").unwrap().is_field_path(&[]));
|
||||
assert!(
|
||||
!NbtPath::parse("blockState[].properties")
|
||||
.unwrap()
|
||||
.is_field_path(&["blockState", "properties"])
|
||||
);
|
||||
}
|
||||
|
||||
fn tag(s: &str) -> Tag {
|
||||
parse_snbt(s).unwrap().0
|
||||
@@ -1020,100 +887,6 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_array_edits_use_the_destination_numeric_accessor() {
|
||||
for (initial, value, inserted, indexed, all) in [
|
||||
(
|
||||
"{v:[B;1b,2b]}",
|
||||
"-3.5f",
|
||||
"{v:[B;1b,-4b,2b]}",
|
||||
"{v:[B;-4b,2b]}",
|
||||
"{v:[B;-4b,-4b]}",
|
||||
),
|
||||
(
|
||||
"{v:[B;1b,2b]}",
|
||||
"-3.5d",
|
||||
"{v:[B;1b,-4b,2b]}",
|
||||
"{v:[B;-4b,2b]}",
|
||||
"{v:[B;-4b,-4b]}",
|
||||
),
|
||||
(
|
||||
"{v:[I;1,2]}",
|
||||
"-3.5f",
|
||||
"{v:[I;1,-4,2]}",
|
||||
"{v:[I;-4,2]}",
|
||||
"{v:[I;-4,-4]}",
|
||||
),
|
||||
(
|
||||
"{v:[I;1,2]}",
|
||||
"-3.5d",
|
||||
"{v:[I;1,-4,2]}",
|
||||
"{v:[I;-4,2]}",
|
||||
"{v:[I;-4,-4]}",
|
||||
),
|
||||
(
|
||||
"{v:[L;1L,2L]}",
|
||||
"-3.5f",
|
||||
"{v:[L;1L,-3L,2L]}",
|
||||
"{v:[L;-3L,2L]}",
|
||||
"{v:[L;-3L,-3L]}",
|
||||
),
|
||||
(
|
||||
"{v:[L;1L,2L]}",
|
||||
"-3.5d",
|
||||
"{v:[L;1L,-4L,2L]}",
|
||||
"{v:[L;-4L,2L]}",
|
||||
"{v:[L;-4L,-4L]}",
|
||||
),
|
||||
] {
|
||||
let value = tag(value);
|
||||
let mut root = tag(initial);
|
||||
assert_eq!(
|
||||
path("v")
|
||||
.insert(1, &mut root, std::slice::from_ref(&value))
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(root, tag(inserted));
|
||||
let mut root = tag(initial);
|
||||
assert_eq!(path("v[0]").set(&mut root, &value).unwrap(), 1);
|
||||
assert_eq!(root, tag(indexed));
|
||||
let mut root = tag(initial);
|
||||
assert_eq!(path("v[]").set(&mut root, &value).unwrap(), 2);
|
||||
assert_eq!(root, tag(all));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_arrays_reject_nonnumeric_insert_and_indexed_set() {
|
||||
for initial in ["{v:[B;1b,2b]}", "{v:[I;1,2]}", "{v:[L;1L,2L]}"] {
|
||||
let original = tag(initial);
|
||||
let value = tag("\"bad\"");
|
||||
for index in [0, 99, -99] {
|
||||
let mut root = original.clone();
|
||||
assert_eq!(
|
||||
path("v")
|
||||
.insert(index, &mut root, std::slice::from_ref(&value))
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(root, original);
|
||||
}
|
||||
let mut root = original.clone();
|
||||
assert_eq!(path("v[0]").set(&mut root, &value).unwrap(), 0);
|
||||
assert_eq!(root, original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_array_wildcard_set_preserves_vanillas_count_on_rejection() {
|
||||
for initial in ["{v:[B;]}", "{v:[I;]}", "{v:[L;]}"] {
|
||||
let mut root = tag(initial);
|
||||
assert_eq!(path("v[]").set(&mut root, &tag("\"bad\"")).unwrap(), 1);
|
||||
assert_eq!(root, tag(initial));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_comparison() {
|
||||
let actual = tag("{a:1,b:{c:[1,2,3],d:\"x\"},e:2}");
|
||||
|
||||
@@ -173,16 +173,6 @@ impl Registries {
|
||||
fill(&mut self.templates, "templates");
|
||||
fill(&mut self.persistent_components, "persistent_components");
|
||||
fill(&mut self.learnable_recipes, "learnable_recipes");
|
||||
if self.summonable.is_empty() {
|
||||
self.summonable = strings(&r["summonable"]);
|
||||
self.summonable.sort();
|
||||
self.summonable.dedup();
|
||||
}
|
||||
if self.entity_types.is_empty() {
|
||||
self.entity_types = strings(&r["entity_types"]);
|
||||
self.entity_types.sort();
|
||||
self.entity_types.dedup();
|
||||
}
|
||||
const RELOADED: [&str; 5] = [
|
||||
"minecraft:advancement",
|
||||
"minecraft:recipe",
|
||||
@@ -246,16 +236,6 @@ impl Registries {
|
||||
fill(&mut self.templates, &e.templates);
|
||||
fill(&mut self.persistent_components, &e.persistent_components);
|
||||
fill(&mut self.learnable_recipes, &e.learnable_recipes);
|
||||
if self.summonable.is_empty() {
|
||||
self.summonable.clone_from(&e.summonable);
|
||||
self.summonable.sort();
|
||||
self.summonable.dedup();
|
||||
}
|
||||
if self.entity_types.is_empty() {
|
||||
self.entity_types.clone_from(&e.entity_types);
|
||||
self.entity_types.sort();
|
||||
self.entity_types.dedup();
|
||||
}
|
||||
for (name, ids) in &e.by_registry {
|
||||
if !self.by_registry.contains_key(name) {
|
||||
self.by_registry.insert(name.clone(), ids.clone());
|
||||
@@ -275,25 +255,6 @@ impl Registries {
|
||||
.clone_from(&e.advancement_criteria);
|
||||
}
|
||||
}
|
||||
/// Add the damage catalog's types ([`Self::damage_types`]: content mods'
|
||||
/// and the world's packs' with vanilla's) to the commands'
|
||||
/// `minecraft:damage_type` ids, which the export lists for vanilla alone.
|
||||
/// `ResourceArgument` looks a type up in the server's registries, so
|
||||
/// `/damage` takes every type the world has. The export's tags stay. The
|
||||
/// ids are replaced, not extended: `by_registry` outlives a world, and a
|
||||
/// pack's type must not stay accepted in the next world opened.
|
||||
pub fn sync_damage_type_ids(&mut self) {
|
||||
if self.damage_types.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut ids = self.damage_types.clone();
|
||||
ids.sort();
|
||||
ids.dedup();
|
||||
self.by_registry
|
||||
.entry("minecraft:damage_type".into())
|
||||
.or_default()
|
||||
.ids = ids;
|
||||
}
|
||||
/// The element ids of a registry (sorted).
|
||||
pub fn ids(&self, registry: &str) -> &[String] {
|
||||
if let Some(r) = self.by_registry.get(registry) {
|
||||
@@ -331,49 +292,3 @@ impl Registries {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Registries;
|
||||
|
||||
/// A content mod's damage type joins vanilla's in the commands' ids, the
|
||||
/// export's tags stay, and a second world open lists each type once.
|
||||
#[test]
|
||||
fn damage_type_ids_take_the_catalogs_types() {
|
||||
let export = Registries::from_export(&serde_json::json!({
|
||||
"by_registry": {"minecraft:damage_type": {
|
||||
"ids": ["minecraft:fall", "minecraft:magic"],
|
||||
"tags": ["minecraft:is_fall"],
|
||||
}},
|
||||
}));
|
||||
let mut reg = Registries {
|
||||
damage_types: vec![
|
||||
"minecraft:magic".into(),
|
||||
"example:test".into(),
|
||||
"minecraft:fall".into(),
|
||||
],
|
||||
..Registries::default()
|
||||
};
|
||||
reg.fill_from_exported(&export);
|
||||
assert_eq!(
|
||||
reg.ids("minecraft:damage_type"),
|
||||
["minecraft:fall", "minecraft:magic"],
|
||||
"the export alone lists vanilla's types"
|
||||
);
|
||||
for _ in 0..2 {
|
||||
reg.sync_damage_type_ids();
|
||||
assert_eq!(
|
||||
reg.ids("minecraft:damage_type"),
|
||||
["example:test", "minecraft:fall", "minecraft:magic"]
|
||||
);
|
||||
assert_eq!(reg.tags("minecraft:damage_type"), ["minecraft:is_fall"]);
|
||||
}
|
||||
// The next world, without the pack: its type is no longer accepted.
|
||||
reg.damage_types = vec!["minecraft:fall".into(), "minecraft:magic".into()];
|
||||
reg.sync_damage_type_ids();
|
||||
assert_eq!(
|
||||
reg.ids("minecraft:damage_type"),
|
||||
["minecraft:fall", "minecraft:magic"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -964,10 +964,6 @@ impl Scoreboard {
|
||||
pub fn tracked(&self) -> Vec<String> {
|
||||
self.holder_order.stream().map(str::to_owned).collect()
|
||||
}
|
||||
/// Every holder on a team (`teamsByPlayer.keySet()`).
|
||||
pub fn team_members(&self) -> impl Iterator<Item = &str> {
|
||||
self.team_of.keys().map(String::as_str)
|
||||
}
|
||||
|
||||
// teams
|
||||
pub fn team(&self, name: &str) -> Option<&Team> {
|
||||
|
||||
@@ -8,20 +8,11 @@ pub use horizon_world::snbt_text::{java_double, java_float, quote_and_escape, to
|
||||
|
||||
// ---- the coloured form -----------------------------------------------------
|
||||
|
||||
/// Writer provenance for a live moving-piston command read, or its selected subtree.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum PistonPart {
|
||||
Entity,
|
||||
State,
|
||||
Properties,
|
||||
}
|
||||
|
||||
struct Pretty {
|
||||
indentation: String,
|
||||
result: Text,
|
||||
indent_depth: usize,
|
||||
depth: usize,
|
||||
piston: Option<PistonPart>,
|
||||
}
|
||||
fn is_numeric(t: &Tag) -> bool {
|
||||
matches!(
|
||||
@@ -67,10 +58,6 @@ impl Pretty {
|
||||
self.lit("]", None);
|
||||
}
|
||||
fn sub(&mut self, tag: &Tag, indent: bool) {
|
||||
self.sub_with(tag, indent, None);
|
||||
}
|
||||
fn sub_with(&mut self, tag: &Tag, indent: bool, piston: Option<PistonPart>) {
|
||||
let previous = std::mem::replace(&mut self.piston, piston);
|
||||
if indent {
|
||||
self.indent_depth += 1;
|
||||
}
|
||||
@@ -80,7 +67,6 @@ impl Pretty {
|
||||
self.indent_depth -= 1;
|
||||
}
|
||||
self.depth -= 1;
|
||||
self.piston = previous;
|
||||
}
|
||||
fn visit(&mut self, tag: &Tag) {
|
||||
match tag {
|
||||
@@ -171,11 +157,7 @@ impl Pretty {
|
||||
"\n"
|
||||
};
|
||||
let n = c.len();
|
||||
let entries = match self.piston {
|
||||
Some(part) => piston_hash_order(c, part),
|
||||
None => java_hash_order(c),
|
||||
};
|
||||
for (i, (k, v)) in entries.into_iter().enumerate() {
|
||||
for (i, (k, v)) in java_hash_order(c).into_iter().enumerate() {
|
||||
self.lit(&entry, None);
|
||||
let key = string_of(k);
|
||||
if !key.is_empty()
|
||||
@@ -189,12 +171,7 @@ impl Pretty {
|
||||
}
|
||||
self.lit(":", None);
|
||||
self.lit(" ", None);
|
||||
let child = match (self.piston, key.as_str()) {
|
||||
(Some(PistonPart::Entity), "blockState") => Some(PistonPart::State),
|
||||
(Some(PistonPart::State), "properties") => Some(PistonPart::Properties),
|
||||
_ => None,
|
||||
};
|
||||
self.sub_with(v, true, child);
|
||||
self.sub(v, true);
|
||||
if i + 1 < n {
|
||||
self.lit(",", None);
|
||||
self.lit(spacing, None);
|
||||
@@ -209,61 +186,22 @@ impl Pretty {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn hash_bucket(len: usize, key: &horizon_world::nbt::JavaString) -> usize {
|
||||
let mut capacity = 16usize;
|
||||
while len > capacity * 3 / 4 {
|
||||
capacity *= 2;
|
||||
}
|
||||
let h = string_of(key)
|
||||
.encode_utf16()
|
||||
.fold(0u32, |h, ch| h.wrapping_mul(31).wrapping_add(u32::from(ch)));
|
||||
((h ^ (h >> 16)) as usize) & (capacity - 1)
|
||||
}
|
||||
fn piston_hash_order(
|
||||
c: &horizon_world::nbt::Compound,
|
||||
part: PistonPart,
|
||||
) -> Vec<(&horizon_world::nbt::JavaString, &Tag)> {
|
||||
let mut entries: Vec<_> = c.iter().collect();
|
||||
match part {
|
||||
PistonPart::Entity => {
|
||||
// saveAdditional, then the base's components and saveMetadata.
|
||||
let fields = [
|
||||
"blockState",
|
||||
"facing",
|
||||
"progress",
|
||||
"extending",
|
||||
"source",
|
||||
"components",
|
||||
"id",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
];
|
||||
entries.sort_by_key(|(k, _)| {
|
||||
(
|
||||
hash_bucket(c.len(), k),
|
||||
fields
|
||||
.iter()
|
||||
.position(|s| *s == string_of(k))
|
||||
.unwrap_or(fields.len()),
|
||||
)
|
||||
});
|
||||
}
|
||||
PistonPart::Properties => {
|
||||
// StateDefinition builds property codecs in sorted-name order.
|
||||
// DFU's nested map-pair encoder writes the second codec first.
|
||||
entries.sort_by_key(|(k, _)| (hash_bucket(c.len(), k), std::cmp::Reverse(*k)));
|
||||
}
|
||||
PistonPart::State => return java_hash_order(c),
|
||||
}
|
||||
entries
|
||||
}
|
||||
/// The iteration order of the `HashMap<String, Tag>` a `CompoundTag` holds,
|
||||
/// for a map that grew to its size by insertion: buckets by
|
||||
/// `HashMap.hash(String.hashCode())`, keys sharing a bucket in key order.
|
||||
pub fn java_hash_order(
|
||||
c: &horizon_world::nbt::Compound,
|
||||
) -> Vec<(&horizon_world::nbt::JavaString, &Tag)> {
|
||||
let mut capacity = 16usize;
|
||||
while c.len() > capacity * 3 / 4 {
|
||||
capacity *= 2;
|
||||
}
|
||||
let bucket = |k: &horizon_world::nbt::JavaString| {
|
||||
let h = string_of(k)
|
||||
.encode_utf16()
|
||||
.fold(0u32, |h, ch| h.wrapping_mul(31).wrapping_add(u32::from(ch)));
|
||||
((h ^ (h >> 16)) as usize) & (capacity - 1)
|
||||
};
|
||||
// Keys sharing a bucket iterate in insertion order: the order vanilla's
|
||||
// savers write them, as far as it is known here.
|
||||
const WRITE_ORDER: &[&str] = &[
|
||||
@@ -330,7 +268,7 @@ pub fn java_hash_order(
|
||||
.unwrap_or(WRITE_ORDER.len())
|
||||
};
|
||||
let mut entries: Vec<_> = c.iter().collect();
|
||||
entries.sort_by_key(|(k, _)| (hash_bucket(c.len(), k), rank(k)));
|
||||
entries.sort_by_key(|(k, _)| (bucket(k), rank(k)));
|
||||
entries
|
||||
}
|
||||
/// `NbtUtils.toPrettyComponent`: the coloured, unindented form.
|
||||
@@ -343,19 +281,6 @@ pub fn pretty_indented(tag: &Tag, indentation: &str) -> Text {
|
||||
result: Text::empty(),
|
||||
indent_depth: 0,
|
||||
depth: 0,
|
||||
piston: None,
|
||||
};
|
||||
p.visit(tag);
|
||||
p.result
|
||||
}
|
||||
/// Command-only writer order; ordinary pretty printing keeps its existing policy.
|
||||
pub fn pretty_piston(tag: &Tag, part: PistonPart) -> Text {
|
||||
let mut p = Pretty {
|
||||
indentation: String::new(),
|
||||
result: Text::empty(),
|
||||
indent_depth: 0,
|
||||
depth: 0,
|
||||
piston: Some(part),
|
||||
};
|
||||
p.visit(tag);
|
||||
p.result
|
||||
@@ -370,33 +295,6 @@ mod tests {
|
||||
to_snbt(&parse_snbt(s).unwrap().0)
|
||||
}
|
||||
#[test]
|
||||
fn live_piston_writer_order_does_not_change_default_pretty() {
|
||||
let tag = parse_snbt(
|
||||
r#"{id:"minecraft:piston",blockState:{id:"minecraft:piston_head",properties:{facing:"east",short:"false",type:"normal"}},components:{},x:1,y:100,z:0,facing:5b,progress:0.0f,source:1b,extending:1b}"#,
|
||||
).unwrap().0;
|
||||
let ordinary = r#"{blockState: {id: "minecraft:piston_head", properties: {facing: "east", short: "false", type: "normal"}}, components: {}, x: 1, facing: 5b, progress: 0.0f, y: 100, z: 0, id: "minecraft:piston", source: 1b, extending: 1b}"#;
|
||||
assert_eq!(pretty(&tag).plain(), ordinary);
|
||||
assert_eq!(
|
||||
pretty_piston(&tag, PistonPart::Entity).plain(),
|
||||
r#"{blockState: {id: "minecraft:piston_head", properties: {short: "false", facing: "east", type: "normal"}}, components: {}, x: 1, facing: 5b, progress: 0.0f, y: 100, z: 0, source: 1b, id: "minecraft:piston", extending: 1b}"#,
|
||||
);
|
||||
let state = tag.get("blockState").unwrap();
|
||||
assert_eq!(
|
||||
pretty_piston(state, PistonPart::State).plain(),
|
||||
r#"{id: "minecraft:piston_head", properties: {short: "false", facing: "east", type: "normal"}}"#,
|
||||
);
|
||||
let props = state.get("properties").unwrap();
|
||||
assert_eq!(
|
||||
pretty_piston(props, PistonPart::Properties).plain(),
|
||||
r#"{short: "false", facing: "east", type: "normal"}"#,
|
||||
);
|
||||
assert_eq!(
|
||||
pretty(props).plain(),
|
||||
r#"{facing: "east", short: "false", type: "normal"}"#
|
||||
);
|
||||
assert_eq!(pretty(&tag).plain(), ordinary);
|
||||
}
|
||||
#[test]
|
||||
fn compact_form_matches_vanilla() {
|
||||
assert_eq!(
|
||||
rt("{b:1b,a:\"x\",c:[1,2],d:1.5f,e:2d,f:3L,g:4s}"),
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "horizon-client-input"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
rust-version = "1.90"
|
||||
description = "The client's input layer: key bindings and their names, mouse, controllers, clipboard and window options"
|
||||
|
||||
[dependencies]
|
||||
|
||||