mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2818c89f24 | ||
|
|
9821bb7ef5 | ||
|
|
b9f3821f24 | ||
|
|
4ff23b686d | ||
|
|
30f0c715b2 | ||
|
|
5f12705a29 |
No files matched your search
@@ -2,6 +2,7 @@
|
||||
target/
|
||||
.local/
|
||||
**/node_modules/
|
||||
website/
|
||||
benchmarks/
|
||||
packs/
|
||||
mods/
|
||||
@@ -1,6 +0,0 @@
|
||||
# Generated files that tests compare byte for byte with what they generate: LF on every OS, also
|
||||
# where Git for Windows' core.autocrlf would check them out with CRLF.
|
||||
docs/launcher-commands.md text eol=lf
|
||||
docs/horizon-ctl-commands.md text eol=lf
|
||||
crates/horizon-theme/tokens.css text eol=lf
|
||||
docs/api/*-reference.md text eol=lf
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec python3 tools/check_public_tree.py --pre-push
|
||||
exec python3 tools/check_public_tree.py --history
|
||||
@@ -1,16 +0,0 @@
|
||||
# Changes to these files wait for Ludovic's review once branch protection requires code owners' reviews
|
||||
# (tools/ci/protect_pipeline.sh, docs/security.md). They are what builds, signs and ships releases, the code that
|
||||
# checks downloads before installing them (backend.rs downloads, installs and updates the launcher itself; links.json
|
||||
# says where from; the Cargo.toml files pick the crates that do it), and this file. Everything else merges as soon as
|
||||
# `verify` passes.
|
||||
/.github/ @ludovic111
|
||||
/tools/ci/ @ludovic111
|
||||
/tools/release_train.py @ludovic111
|
||||
/crates/horizon-release-sign/ @ludovic111
|
||||
/crates/horizon-server/src/update.rs @ludovic111
|
||||
/crates/horizon-server/Cargo.toml @ludovic111
|
||||
/launcher/src/signed.rs @ludovic111
|
||||
/launcher/src/backend.rs @ludovic111
|
||||
/launcher/links.json @ludovic111
|
||||
/launcher/Cargo.toml @ludovic111
|
||||
/packaging/ @ludovic111
|
||||
@@ -1,7 +0,0 @@
|
||||
# Trusted reviewers: they pick the pull requests the Nightly holds with `/nightly 2640 2642` in any issue or pull
|
||||
# request (docs/release-flow.md "Nightly channel", tools/ci/nightly_set.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 every reviewer's `/nightly` (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)
|
||||
@@ -1 +0,0 @@
|
||||
0.2
|
||||
@@ -1,27 +0,0 @@
|
||||
name: cancel-on-close
|
||||
# A closed or merged pull request's queued and running checks free the runners at once.
|
||||
# pull_request_target so the token can cancel runs of pull requests from forks too;
|
||||
# it checks out nothing and runs no code from the pull request.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
cancel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel the pull request's unfinished runs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
for status in queued in_progress waiting pending requested; do
|
||||
gh run list -R "$REPO" --branch "$BRANCH" --status "$status" --limit 100 \
|
||||
--json databaseId,event,headSha \
|
||||
-q ".[] | select(.event == \"pull_request\" and .headSha == \"$SHA\") | .databaseId"
|
||||
done | sort -u | while read -r id; do
|
||||
gh run cancel -R "$REPO" "$id" || true
|
||||
done
|
||||
+129
-91
@@ -1,30 +1,23 @@
|
||||
name: checks
|
||||
# verify: every pull request and every push to main, on GitHub's runners (the repository is public). It is the
|
||||
# check main requires, so it never has a condition: a skipped job counts as passed, and every run of this workflow
|
||||
# must report a real verify (a pull request that changes no Rust finishes it in seconds instead).
|
||||
# verify: every pull request and every push to main, on GitHub's runners (the repository is public).
|
||||
# parity: the vanilla harnesses a pull request's changes can affect, on the Mac mini's runner, which holds the
|
||||
# maintainer's own Minecraft files (Horizon never downloads the game); .github/workflows/parity.yml. Only for a pull
|
||||
# request that changes a harness, oracle, probe or the runner: the rest are checked on main by parity-main. A fork's pull
|
||||
# request runs it only when a maintainer adds the run-parity label (.github/workflows/run-parity.yml), so labels
|
||||
# never start this workflow. See docs/release-flow.md.
|
||||
# maintainer's own Minecraft files (Horizon never downloads the game). A fork's pull request runs it only when
|
||||
# a maintainer adds the run-parity label, once per label. See docs/release-flow.md.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
# A new push to a pull request replaces its running checks; main's runs all finish (releases follow them).
|
||||
group: checks-${{ github.event.pull_request.number || github.sha }}
|
||||
group: checks-${{ github.event.pull_request.number || github.sha }}-${{ github.event.action == 'labeled' && 'label' || 'run' }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
if: github.event.action != 'labeled'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
@@ -62,92 +55,43 @@ jobs:
|
||||
if: steps.rust.outputs.required != 'false'
|
||||
run: |
|
||||
cargo +stable fmt --all -- --check
|
||||
# With the game's code mods (on in every release build, off by default).
|
||||
cargo +stable clippy --workspace --all-targets --features horizon-client/mods -- -D warnings
|
||||
# The server build (released as horizon-engine): the game without sound or controllers.
|
||||
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
|
||||
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
|
||||
# type-checks and the parity job builds and runs (they were most of this step's build and link time).
|
||||
cargo +stable clippy --workspace --all-targets -- -D warnings
|
||||
- name: Test Rust core and voxel kernel correctness
|
||||
if: steps.rust.outputs.required != 'false'
|
||||
run: |
|
||||
cargo +stable test --workspace --lib --bins --tests --features horizon-client/mods,horizon-mods/pack
|
||||
# The full content-root tests (#[ignore]d: minutes each on a laptop). The same build as above; they need the
|
||||
# prepared vanilla data, so without it they pass at once.
|
||||
cargo +stable test --workspace --lib --features horizon-client/mods,horizon-mods/pack mod_content::tests:: -- --ignored
|
||||
cargo +stable test --workspace --doc --features horizon-client/mods,horizon-mods/pack
|
||||
# The launcher is its own workspace (launcher/Cargo.toml), so verify's cargo runs never build it. A pull request that
|
||||
# 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: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Does this pull request change the launcher or a crate it builds?
|
||||
if: github.event_name == 'pull_request'
|
||||
id: launcher
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: python3 tools/ci/rust_changes.py --launcher "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
||||
- name: Install Rust and the headers GPUI links against
|
||||
if: steps.launcher.outputs.required != 'false'
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libxkbcommon-x11-dev libwayland-dev \
|
||||
libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev libudev-dev
|
||||
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: steps.launcher.outputs.required != 'false'
|
||||
with:
|
||||
workspaces: launcher
|
||||
shared-key: launcher
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Check the launcher's formatting, lint and tests
|
||||
if: steps.launcher.outputs.required != 'false'
|
||||
working-directory: launcher
|
||||
run: |
|
||||
cargo +stable fmt --check
|
||||
cargo +stable clippy --all-targets --locked -- -D warnings
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
run: ${{ steps.scope.outputs.run }}
|
||||
run: cargo +stable test --workspace
|
||||
parity:
|
||||
if: >-
|
||||
github.event_name == 'pull_request' && (
|
||||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.action != 'labeled') ||
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'run-parity'))
|
||||
runs-on: [self-hosted, horizon-game]
|
||||
timeout-minutes: 120 # a change to core crates reruns ~300 harnesses: ~60-90 min on the Mac mini
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The commit the event names: a fork's later pushes need another run-parity label.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- id: scope
|
||||
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Tools from the runner
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Vanilla data, and resources prepared by this preparer
|
||||
run: bash tools/ci/vanilla-data.sh
|
||||
- name: The harnesses this change can affect
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
|
||||
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 "* ]]; then
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
echo "The pull request runs its harnesses: ${harness:-label run-parity}"
|
||||
else
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No harness, oracle, probe or runner change: parity-main checks this on main"
|
||||
fi
|
||||
parity:
|
||||
needs: parity-scope
|
||||
if: needs.parity-scope.outputs.run == 'true'
|
||||
uses: ./.github/workflows/parity.yml
|
||||
with:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
|
||||
PARITY_HOSTS: ''
|
||||
run: python3 tools/parity.py --changed ${{ github.event.pull_request.base.sha }} --timeout 2400 # world_spawn takes ~1480 s on a busy runner
|
||||
- name: Logs of the failed harnesses
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: parity-logs
|
||||
path: .local/parity-runner/logs/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
# 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
|
||||
@@ -500,7 +444,7 @@ jobs:
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: stable
|
||||
run: python tools/test_float_log_parity.py
|
||||
jfr-analysis:
|
||||
portable-mod:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: [self-hosted, linux, horizon-game]
|
||||
steps:
|
||||
@@ -512,7 +456,101 @@ jobs:
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
- name: Compile original mod and test metrics
|
||||
run: python tools/build_mod.py
|
||||
- name: Verify streaming JFR analysis against the JDK exporter
|
||||
run: python tools/test_jfr.py
|
||||
- name: Verify ZGC pause and allocation event interpretation
|
||||
run: python tools/test_jfr.py --gc zgc
|
||||
- name: Build pinned portable packs
|
||||
run: python tools/build_pack.py
|
||||
- name: Measure isolated worker pacing on this platform
|
||||
run: python -c "from tools.build_mod import benchmark_workers; benchmark_workers()"
|
||||
- name: Measure isolated debounce queue polling
|
||||
run: python -c "from tools.build_mod import benchmark_debounce; benchmark_debounce()"
|
||||
- name: Measure wakeable idle queue latency
|
||||
run: python -c "from tools.build_mod import benchmark_idle; benchmark_idle()"
|
||||
- name: Measure object allocations with compact headers off and on
|
||||
run: python -c "from tools.build_mod import benchmark_headers; benchmark_headers()"
|
||||
- name: Test pack integrity and comparisons
|
||||
run: python -m unittest discover -s tests -v
|
||||
- name: Start recommended OpenGL macOS clients and verify framebuffer readback
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
python tools/test_client_graphics.py --native-display
|
||||
cp dist/client-graphics-smoke.json dist/client-graphics-macos-title.json
|
||||
python tools/test_client_graphics.py --native-display --world
|
||||
cp dist/client-graphics-smoke.json dist/client-graphics-macos-world.json
|
||||
- name: Compile the private dispatch fixture
|
||||
run: python -c "from tools.build_mod import build_dispatch_fixture; build_dispatch_fixture()"
|
||||
- name: Compile the private integrated-server graphics fixture
|
||||
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('server-graphics', 'dev.horizonlab.metrics.ServerGraphicsIntegration', fixture_dependencies={})"
|
||||
- name: Compile the private tick-manager state fixture
|
||||
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('tick-state', 'dev.horizonlab.metrics.TickStateIntegration')"
|
||||
- name: Compile the private terrain tick membership fixture
|
||||
run: python -c "from tools.build_mod import build_tick_dedup_fixture; build_tick_dedup_fixture()"
|
||||
- name: Compile the private culling whitelist fixture
|
||||
run: python -c "from tools.build_mod import build_culling_empty_fixture; build_culling_empty_fixture()"
|
||||
- name: Compile the private culling ray geometry fixture
|
||||
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('culling-rays', 'dev.horizonlab.dispatch.CullingRayIntegration', ['entityculling'], ['CullingRayAccess'])"
|
||||
- name: Compile the private terrain position lookup fixture
|
||||
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-position', 'dev.horizonlab.dispatch.PositionIntegration')"
|
||||
- name: Compile the private terrain quad reuse fixture
|
||||
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('quad-reuse', 'dev.horizonlab.dispatch.QuadReuseIntegration')"
|
||||
- name: Compile the private object-size agent and terrain quad fixture
|
||||
run: python -c "from tools.test_quad_sizes import build_agent; from tools.build_mod import build_integration_fixture; build_agent(); build_integration_fixture('quad-size', 'dev.horizonlab.dispatch.QuadSizeIntegration')"
|
||||
- name: Compile the private terrain read shutdown fixture
|
||||
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-read', 'dev.horizonlab.dispatch.TerrainReadIntegration')"
|
||||
- name: Install headless graphics libraries for applied-hook checks
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb libgl1 libegl1 libvulkan1 mesa-vulkan-drivers libasound2t64 libxkbcommon0
|
||||
- name: Verify integrated-server graphics report against the actual device
|
||||
run: python tools/test_server_graphics.py ${{ runner.os == 'macOS' && '--native-display' || '' }}
|
||||
- name: Exercise the applied dispatcher hooks and upstream rejection path
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_dispatch.py
|
||||
- name: Verify applied tick membership transitions and allocation
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_tick_dedup.py
|
||||
- name: Verify live culling whitelist registration and empty iteration
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_culling_empty.py
|
||||
- name: Compare applied culling ray geometry and provider queries
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_culling_rays.py
|
||||
- name: Verify actual full-data position lookup equivalence
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_terrain_position.py
|
||||
- name: Verify actual terrain quad geometry and allocation
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_quad_reuse.py
|
||||
- name: Measure actual builder and quad shallow sizes in both header modes
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_quad_sizes.py
|
||||
- name: Verify interrupted DH reads and preserved shared futures
|
||||
if: runner.os == 'Linux'
|
||||
run: python tools/test_terrain_read.py
|
||||
- name: Upload original mod and pack manifests only
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: horizon-linux
|
||||
path: |
|
||||
dist/*.mrpack
|
||||
dist/horizon-*.jar
|
||||
dist/worker-pacing-benchmark.txt
|
||||
dist/debounce-queue-benchmark.txt
|
||||
dist/idle-queue-benchmark.txt
|
||||
dist/object-header-benchmark.txt
|
||||
dist/dispatch-integration.json
|
||||
dist/tick-dedup-integration.json
|
||||
dist/culling-empty-integration.json
|
||||
dist/terrain-read-integration.json
|
||||
dist/culling-rays-integration.json
|
||||
dist/terrain-position-integration.json
|
||||
dist/quad-reuse-integration.json
|
||||
dist/quad-size-integration.json
|
||||
dist/server-graphics-smoke.json
|
||||
dist/client-graphics-smoke.json
|
||||
dist/client-graphics-macos-title.json
|
||||
dist/client-graphics-macos-world.json
|
||||
retention-days: 3
|
||||
@@ -1,291 +0,0 @@
|
||||
name: game build
|
||||
# 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). 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,
|
||||
# which never runs what this one built. See docs/release-flow.md.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: The commit to build
|
||||
type: string
|
||||
required: true
|
||||
version:
|
||||
description: The version built in (HORIZON_VERSION), e.g. 0.2.81 or nightly-2026-10-28
|
||||
type: string
|
||||
required: true
|
||||
nightly:
|
||||
description: A nightly's "<date> <short sha>" (HORIZON_NIGHTLY), shown by the game; empty for a release
|
||||
type: string
|
||||
default: ''
|
||||
trusted:
|
||||
# Only release.yml says true: it builds main. A nightly can hold pull requests nobody reviewed yet, whose
|
||||
# build scripts and tests run here and could rewrite target/, the cargo home or a compiler cache. So an
|
||||
# untrusted build never runs on the release runner, never saves the Rust cache (its key is its own anyway),
|
||||
# and a trusted one never reads the machine's shared sccache, which pull requests' jobs write
|
||||
# (tests/test_nightly.py pins all of it).
|
||||
description: The source is main (a release); false for anything else (a nightly)
|
||||
type: boolean
|
||||
default: false
|
||||
developer_id:
|
||||
description: Sign the macOS programs with Developer ID and notarize the zip (the runner's keychain)
|
||||
type: boolean
|
||||
default: true
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
exporters:
|
||||
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it. An untrusted build
|
||||
# goes to the Mac mini runner that already runs pull requests' parity jobs (macOS + horizon-game:
|
||||
# ludovics-mac-mini), never the release runner (MAC_RUNNER: ludovics-mac-mini-release) nor parity-main's.
|
||||
runs-on: ${{ inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-game"]') }}
|
||||
# Three full preparations (two isolated, one batched) besides the compile.
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
clean: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: bash tools/ci/runner-env.sh
|
||||
env:
|
||||
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
|
||||
- run: bash tools/ci/vanilla-data.sh
|
||||
- name: Compile and validate original exporters against the pinned jars
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
|
||||
# prepares afresh.
|
||||
cargo build --release --locked -p horizon-prepare
|
||||
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
|
||||
# write those (vanilla-data.sh links .local/exporters to them).
|
||||
rm -rf .local/exporters
|
||||
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
|
||||
mkdir -p exporters
|
||||
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
|
||||
# Only our classes and their manifest leave this runner.
|
||||
stray=$(find exporters -type f ! -path exporters/manifest.json \
|
||||
! \( -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@v4
|
||||
with:
|
||||
name: original-exporters
|
||||
path: exporters/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
build:
|
||||
needs: exporters
|
||||
name: build ${{ matrix.name }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: linux
|
||||
os: ubuntu-22.04
|
||||
dir: horizon-linux-x86_64
|
||||
asset: horizon-linux-x86_64.tar.gz
|
||||
# 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: windows
|
||||
os: windows-latest
|
||||
dir: horizon-windows-x64
|
||||
asset: horizon-windows-x64.zip
|
||||
exe: .exe
|
||||
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
|
||||
rustflags: -C target-feature=+crt-static
|
||||
- name: macos
|
||||
mac: true
|
||||
dir: horizon-macos-arm64
|
||||
asset: horizon-macos-arm64.zip
|
||||
runs-on: ${{ matrix.mac && (inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-game"]')) || matrix.os }}
|
||||
# Notarizing the zip can take a few minutes.
|
||||
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
|
||||
# telemetry say it. No version-bump commits.
|
||||
HORIZON_VERSION: ${{ inputs.version }}
|
||||
# A nightly's date and commit ("2026-10-28 0c02067"), which the game shows; empty for a release.
|
||||
HORIZON_NIGHTLY: ${{ inputs.nightly }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
|
||||
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
|
||||
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
|
||||
# keeps its preparations outside the checkout.
|
||||
clean: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
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'
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
|
||||
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
|
||||
run: cargo build --release --locked --features horizon-client/mods -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
|
||||
- name: Every program the release folder takes was built
|
||||
run: |
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
|
||||
done
|
||||
- name: The signing keychain, unlocked
|
||||
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
||||
env:
|
||||
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
|
||||
run: packaging/macos/keychain.sh unlock
|
||||
- name: Signed with Developer ID and the hardened runtime
|
||||
# Before the checks below, so they run the programs as players will.
|
||||
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
||||
env:
|
||||
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
|
||||
run: |
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
|
||||
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
|
||||
codesign --verify --strict --verbose=2 "target/release/$binary"
|
||||
done
|
||||
- name: Only system libraries
|
||||
# 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: |
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
file="target/release/$binary${{ matrix.exe }}"
|
||||
if [[ "$RUNNER_OS" == macOS ]]; then
|
||||
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
|
||||
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
|
||||
else
|
||||
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
|
||||
fi
|
||||
done
|
||||
- name: Every program starts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bin=target/release
|
||||
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
|
||||
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
|
||||
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
|
||||
# The version is the release's.
|
||||
for binary in horizon-prepare horizon-server; do
|
||||
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
|
||||
echo "$out"
|
||||
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
|
||||
done
|
||||
- name: Tools from the runner
|
||||
if: runner.environment == 'self-hosted'
|
||||
env:
|
||||
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Resources prepared from the runner's own Minecraft
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: bash tools/ci/vanilla-data.sh
|
||||
- name: The game runs (offscreen, a new world, 1500 frames)
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
|
||||
home="$RUNNER_TEMP/horizon-smoke"
|
||||
rm -rf "$home" && mkdir -p "$home"
|
||||
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
|
||||
client="$PWD/target/release/horizon-client"
|
||||
resources="$PWD/.local/horizon-client/$version"
|
||||
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
|
||||
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
|
||||
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
|
||||
python3 - "$home/smoke.json" <<'EOF'
|
||||
import json, sys
|
||||
report = json.load(open(sys.argv[1]))
|
||||
print(json.dumps(report))
|
||||
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
|
||||
or not report.get("meshed_sections") or not report.get("player"):
|
||||
sys.exit("the smoke run did not draw a world")
|
||||
EOF
|
||||
# Published with the release; the publish job checks it again before it publishes anything.
|
||||
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: runner.environment == 'self-hosted'
|
||||
with:
|
||||
name: smoke-test.json
|
||||
path: ${{ runner.temp }}/smoke-test.json
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: original-exporters
|
||||
path: exporters
|
||||
- name: Package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo run --locked -p xtask -- package "target/release/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
|
||||
cd dist
|
||||
rm -f "${{ matrix.asset }}"
|
||||
case "${{ matrix.asset }}" in
|
||||
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
|
||||
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
|
||||
else
|
||||
# Without resource forks and extended attributes: plain ditto adds an
|
||||
# AppleDouble `._` file beside every file that has any.
|
||||
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
|
||||
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
|
||||
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
|
||||
fi
|
||||
fi ;;
|
||||
esac
|
||||
ls -l "${{ matrix.asset }}"
|
||||
- name: Notarized
|
||||
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
|
||||
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
||||
run: |
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
|
||||
done
|
||||
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.asset }}
|
||||
path: dist/${{ matrix.asset }}
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: The server build (the game without sound or controllers, as horizon-engine)
|
||||
if: matrix.server_asset
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
|
||||
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
|
||||
# and with code mods like the client.
|
||||
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
|
||||
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
|
||||
folder="dist/${{ matrix.server_dir }}"
|
||||
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
|
||||
"$folder/horizon-engine" --help | grep -q -- '--resources'
|
||||
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
|
||||
cd dist
|
||||
rm -f "${{ matrix.server_asset }}"
|
||||
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
|
||||
ls -l "${{ matrix.server_asset }}"
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: matrix.server_asset
|
||||
with:
|
||||
name: ${{ matrix.server_asset }}
|
||||
path: dist/${{ matrix.server_asset }}
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: The signing keychain, locked again
|
||||
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
||||
run: packaging/macos/keychain.sh lock
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
name: launcher
|
||||
# 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),
|
||||
# push to main that changes the launcher or what it builds in (crates/horizon-prepare, crates/horizon-theme,
|
||||
# crates/horizon-telemetry),
|
||||
# every 10 minutes (a release waiting for merges to settle goes out) and by hand, tools/release_train.py decides as
|
||||
# for the game.
|
||||
# A release is launcher@X.Y.Z on the release line in .github/release-line.txt (docs/versioning.md); without it
|
||||
# launcher-vX.Y.Z, the patch after the last launcher tag or the next minor when a merged pull request is labelled
|
||||
# feature. The version and the tag are given to the build (HORIZON_LAUNCHER_VERSION, HORIZON_LAUNCHER_TAG): no bump
|
||||
# commits.
|
||||
# A release is launcher-vX.Y.Z: the patch after the last launcher tag, or the next minor when a merged pull
|
||||
# request is labelled feature. The version is given to the build (HORIZON_LAUNCHER_VERSION): no bump commits.
|
||||
# Never marked latest. Pushes to launcher branches only build. RELEASES_PAUSED=1 stops releases.
|
||||
# macOS: 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.
|
||||
# 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
|
||||
|
||||
@@ -18,16 +14,10 @@ on:
|
||||
push:
|
||||
branches: [main, launcher, 'launcher-*']
|
||||
paths:
|
||||
# Every crate the launcher builds in, as tools/release_train.py's LAUNCHER_PATHS.
|
||||
- 'launcher/**'
|
||||
- 'crates/horizon-bench/**'
|
||||
- 'crates/horizon-friends/**'
|
||||
- 'crates/horizon-mods/**'
|
||||
- 'crates/horizon-port-pack/**'
|
||||
- 'crates/horizon-prepare/**'
|
||||
- 'crates/horizon-release-sign/**'
|
||||
- 'crates/horizon-telemetry/**'
|
||||
- 'crates/horizon-theme/**'
|
||||
- 'crates/horizon-telemetry/**'
|
||||
- '.github/workflows/launcher.yml'
|
||||
schedule:
|
||||
- cron: '7-59/10 * * * *'
|
||||
@@ -49,10 +39,6 @@ concurrency:
|
||||
group: launcher-${{ github.ref == 'refs/heads/main' && 'release' || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
decide:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -81,8 +67,7 @@ jobs:
|
||||
python3 tools/release_train.py decide --kind launcher ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
|
||||
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
|
||||
tag=$(jq -r '.tag' decision.json)
|
||||
version=${tag#launcher-v}
|
||||
echo "version=${version#launcher@}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${tag#launcher-v}" >> "$GITHUB_OUTPUT"
|
||||
# A dry run (and a launcher branch's push) builds whatever the decision.
|
||||
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then
|
||||
echo "release=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -113,20 +98,15 @@ jobs:
|
||||
# which only the Visual C++ redistributable installs.
|
||||
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 }}
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
# Horizon's Azure app for Microsoft sign-in (docs/microsoft-account.md): a repository
|
||||
# variable, not a secret. Unset, the launcher says sign-in is not configured.
|
||||
HORIZON_MSA_CLIENT_ID: ${{ vars.HORIZON_MSA_CLIENT_ID }}
|
||||
# The mod browser's CurseForge source (docs/mod-browser.md#curseforge): a repository secret. Unset, the
|
||||
# launcher searches Modrinth only.
|
||||
HORIZON_CURSEFORGE_API_KEY: ${{ secrets.HORIZON_CURSEFORGE_API_KEY }}
|
||||
# The release's version and tag, built in (launcher/src/backend.rs VERSION and own_tag: the tag says which
|
||||
# line it is on, docs/versioning.md); a launcher branch's build keeps Cargo.toml's.
|
||||
# The release's version, built in (launcher/src/backend.rs VERSION); a launcher branch's build keeps
|
||||
# Cargo.toml's.
|
||||
HORIZON_LAUNCHER_VERSION: ${{ (github.event_name != 'push' || github.ref == 'refs/heads/main') && needs.decide.outputs.version || '' }}
|
||||
HORIZON_LAUNCHER_TAG: ${{ (github.event_name != 'push' || github.ref == 'refs/heads/main') && needs.decide.outputs.tag || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: launcher
|
||||
@@ -136,9 +116,8 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.decide.outputs.head }}
|
||||
persist-credentials: false
|
||||
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
|
||||
# parity jobs, and nothing they leave may end up in a launcher release (target/ is rebuilt from scratch).
|
||||
clean: true
|
||||
# The Mac mini's runner keeps its target/ between runs.
|
||||
clean: ${{ runner.environment == 'github-hosted' }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
@@ -159,8 +138,7 @@ jobs:
|
||||
echo "::notice::No Metal toolchain on this runner: GPUI compiles its shaders at run time"
|
||||
echo "LAUNCHER_FEATURES=gpui/runtime_shaders" >> "$GITHUB_ENV"
|
||||
fi
|
||||
# Every test target (the library, the binary, tests/), on each system the launcher ships for.
|
||||
- run: cargo test --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
|
||||
- run: cargo test --release --locked --lib ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
|
||||
- run: cargo build --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
|
||||
- name: The version is the release's
|
||||
run: |
|
||||
@@ -192,6 +170,9 @@ jobs:
|
||||
retry "$launcher" install latest
|
||||
"$launcher" versions --installed
|
||||
"$launcher" links --json
|
||||
"$launcher" menu-style classic
|
||||
grep -qx 'menuStyle:classic' "$HORIZON_HOME/horizon.txt"
|
||||
"$launcher" menu-style horizon
|
||||
clients=("$HORIZON_LAUNCHER_HOME"/versions/v*/horizon-client*)
|
||||
client="${clients[0]}"
|
||||
echo "installed client: $client"
|
||||
@@ -207,19 +188,11 @@ jobs:
|
||||
help() { "$client" --help > "$RUNNER_TEMP/help.txt" 2>&1 && head -3 "$RUNNER_TEMP/help.txt" && grep -q -- '--resources' "$RUNNER_TEMP/help.txt"; }
|
||||
soft help
|
||||
rm -rf "$HORIZON_LAUNCHER_HOME"
|
||||
- name: The signing keychain, unlocked
|
||||
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
|
||||
env:
|
||||
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
|
||||
run: ../packaging/macos/keychain.sh unlock
|
||||
- name: Package
|
||||
env:
|
||||
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
|
||||
run: |
|
||||
rm -rf dist out && mkdir -p out
|
||||
if [[ "$RUNNER_OS" == "macOS" ]]; then
|
||||
# Horizon.app, zipped (self-update) and in a .dmg (installer), signed (and notarized with
|
||||
# Developer ID): packaging/macos.
|
||||
# Horizon.app, zipped (self-update) and in a .dmg (installer): packaging/macos.
|
||||
python3 -m venv "$RUNNER_TEMP/dmg" && "$RUNNER_TEMP/dmg/bin/pip" install --quiet dmgbuild==1.6.5
|
||||
version=${HORIZON_LAUNCHER_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}
|
||||
DMGBUILD="$RUNNER_TEMP/dmg/bin/dmgbuild" ../packaging/macos/package.sh target/release/${{ matrix.binary }} "$version" out
|
||||
@@ -239,53 +212,11 @@ jobs:
|
||||
name: ${{ matrix.asset }}
|
||||
path: launcher/out/*
|
||||
retention-days: 3
|
||||
- name: The signing keychain, locked again
|
||||
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
|
||||
run: ../packaging/macos/keychain.sh lock
|
||||
|
||||
sign:
|
||||
release:
|
||||
needs: [decide, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
|
||||
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
|
||||
# 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@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci/sign_release.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
pattern: horizon-launcher-*
|
||||
merge-multiple: true
|
||||
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
|
||||
env:
|
||||
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
|
||||
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
|
||||
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@v4
|
||||
with:
|
||||
name: signature
|
||||
path: dist/SHA256SUMS*
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: needs.decide.outputs.dry_run == 'true'
|
||||
with:
|
||||
name: ephemeral.pub
|
||||
path: dist/ephemeral.pub
|
||||
retention-days: 3
|
||||
|
||||
release:
|
||||
needs: [decide, build, sign]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -303,13 +234,6 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The signature, checked as launchers check it
|
||||
env:
|
||||
TAG: ${{ needs.decide.outputs.tag }}
|
||||
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
run: |
|
||||
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: Publish ${{ needs.decide.outputs.tag }}
|
||||
if: needs.decide.outputs.dry_run != 'true'
|
||||
@@ -317,9 +241,8 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ needs.decide.outputs.tag }}
|
||||
run: |
|
||||
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
|
||||
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
|
||||
--notes-file notes/body.md --latest=false dist/horizon-launcher-* dist/SHA256SUMS "${signature[@]}"
|
||||
--notes-file notes/body.md --latest=false dist/horizon-launcher-*
|
||||
- name: Dry run, the release that would be published
|
||||
if: needs.decide.outputs.dry_run == 'true'
|
||||
run: |
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
name: mod-registry
|
||||
# The submission service is its own workspace (crates/horizon-mod-registry), so the checks job doesn't build
|
||||
# it: this one does, when it or horizon-mods (the checks it shares with the launcher and the game) changes.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'crates/horizon-mods/**'
|
||||
- 'crates/horizon-mod-registry/**'
|
||||
- '.github/workflows/mod-registry.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'crates/horizon-mods/**'
|
||||
- 'crates/horizon-mod-registry/**'
|
||||
- '.github/workflows/mod-registry.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: crates/horizon-mod-registry
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: crates/horizon-mod-registry
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- run: cargo +stable fmt --check
|
||||
- run: cargo +stable clippy --all-targets --locked -- -D warnings
|
||||
- run: cargo +stable test --locked
|
||||
@@ -1,237 +0,0 @@
|
||||
name: nightly build
|
||||
run-name: nightly build${{ inputs.request && format(' (request {0})', inputs.request) || '' }}
|
||||
# The opt-in Nightly channel (docs/release-flow.md "Nightly"; Ludovic, 2026-10-27). Once a night, and when a trusted
|
||||
# reviewer asks (`/nightly 2640 2642`, nightly-command.yml), it builds main plus the pull requests the reviewers
|
||||
# picked, merged in order (tools/ci/nightly_set.py). With none picked, it builds the newest union branch on origin
|
||||
# (union/<agent>-<name>: main plus pull requests the contribution gate proved together), or main when no union is
|
||||
# newer. It publishes the result as the `nightly` pre-release. Launchers install it only for players who turn Nightly
|
||||
# on. The previous nightly stays a week as nightly-<date>.
|
||||
#
|
||||
# Why the signing key never meets the pull requests' code:
|
||||
# - `pick` merges the picked heads onto main with `git merge-tree --write-tree`: git objects only, no working tree,
|
||||
# so none of their files reaches the disk and no hook, filter or script of theirs runs. It refuses any pull
|
||||
# request that changes a path CODEOWNERS gives to Ludovic (this folder, tools/ci/, the signing and update code),
|
||||
# pushes the merge as nightly/build-<date>-<n> and saves the set on the nightly-state branch.
|
||||
# - This file, game-build.yml and every script below run from main's copy: a schedule runs the default branch, and
|
||||
# `uses: ./…` takes the caller's commit. The union's own .github/ is never read.
|
||||
# - `build` (game-build.yml, `trusted: false`) never reaches a release's tools: its macOS jobs run on the runner
|
||||
# that already runs pull requests' parity jobs, never the release runner; its Rust cache has its own key and is
|
||||
# never saved; and releases don't read the machine's shared sccache.
|
||||
# - `build` (game-build.yml) checks out and compiles the merge (or the union) with no secrets: its token reads contents only and
|
||||
# isn't kept in the checkout; the macOS programs are signed ad hoc (Developer ID needs the runner's keychain,
|
||||
# which stays locked for nightlies).
|
||||
# - `sign` is the only job with RELEASE_SIGNING_KEY. It checks out nothing but main's tools/ci/sign_release.sh, which
|
||||
# runs the runner's sha256sum and OpenSSL over the archives: it hashes them, never runs them. The signed message
|
||||
# names the channel ("horizon release nightly"), so a nightly's signature can't pass for a release's.
|
||||
# - `publish` verifies the signature with main's horizon-release-sign, checks the smoke test, and only then moves
|
||||
# the tag. Like sign, it never executes what build produced.
|
||||
# gh workflow run "nightly build" # now (skips when the pick is the same commit as last night's)
|
||||
# gh workflow run "nightly build" -f force=true # build and publish even if unchanged
|
||||
# gh workflow run "nightly build" -f dry_run=true # build and sign with a throwaway key, publish nothing
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 20 * * *' # 20:17 UTC: after the day's waves, before the 03:07 Paris parity audit takes the Mac mini
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: Build and publish even if tonight's pick is the commit already published
|
||||
type: boolean
|
||||
default: false
|
||||
dry_run:
|
||||
description: Build and sign with a throwaway key, publish nothing
|
||||
type: boolean
|
||||
default: false
|
||||
issue:
|
||||
description: The issue or pull request whose `/nightly` comment asked for this build (answered there)
|
||||
type: string
|
||||
default: ''
|
||||
request:
|
||||
description: The command's id, in the run's name so it can link the run
|
||||
type: string
|
||||
default: ''
|
||||
prs:
|
||||
description: A dry run's own set, `2640@<head> 2642@<head>` (the saved set stays)
|
||||
type: string
|
||||
default: ''
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
# One build at a time. A newer request replaces one still waiting: it builds the set as it is by then, so no pick
|
||||
# is lost.
|
||||
group: nightly-build
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
pick:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write # nightly/build-<date>-<n> and the nightly-state branch (main is protected)
|
||||
issues: write # says in the thread when the picked pull requests conflict
|
||||
pull-requests: write
|
||||
outputs:
|
||||
build: ${{ steps.pick.outputs.build }}
|
||||
sha: ${{ steps.pick.outputs.sha }}
|
||||
short: ${{ steps.pick.outputs.short }}
|
||||
source: ${{ steps.pick.outputs.source }}
|
||||
date: ${{ steps.pick.outputs.date }}
|
||||
version: ${{ steps.pick.outputs.version }}
|
||||
nightly: ${{ steps.pick.outputs.nightly }}
|
||||
dry_run: ${{ steps.mode.outputs.dry_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- id: mode
|
||||
# A dry run never publishes; a hand-started run off main is always one.
|
||||
run: echo "dry_run=${{ github.event_name == 'workflow_dispatch' && (inputs.dry_run || github.ref != 'refs/heads/main') }}" >> "$GITHUB_OUTPUT"
|
||||
- id: pick
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FORCE: ${{ (inputs.force || steps.mode.outputs.dry_run == 'true') && 'yes' || '' }}
|
||||
DRY_RUN: ${{ steps.mode.outputs.dry_run }}
|
||||
NIGHTLY_ISSUE: ${{ inputs.issue }}
|
||||
NIGHTLY_PRS: ${{ inputs.prs }}
|
||||
run: |
|
||||
python3 tools/ci/nightly.py pick ${FORCE:+--force} | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
{ echo; cat nightly-notes.md; } >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Old nightly/build-* branches deleted (scheduled runs only)
|
||||
# Ludovic, 2026-10-27: older than 7 days, but never the newest build or one a published nightly points to;
|
||||
# nothing but nightly/build-* (nightly.py stale_builds, tests/test_nightly.py).
|
||||
if: github.event_name == 'schedule'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
{ echo "### nightly/build-* branches"; python3 tools/ci/nightly.py prune-builds --days 7; } | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-notes
|
||||
path: nightly-notes.md
|
||||
retention-days: 3
|
||||
|
||||
build:
|
||||
needs: pick
|
||||
if: needs.pick.outputs.build == 'true'
|
||||
uses: ./.github/workflows/game-build.yml
|
||||
with:
|
||||
ref: ${{ needs.pick.outputs.sha }}
|
||||
version: ${{ needs.pick.outputs.version }}
|
||||
nightly: ${{ needs.pick.outputs.nightly }}
|
||||
developer_id: false
|
||||
trusted: false # it can hold unreviewed pull requests: their own cache key, never saved, not the release runner
|
||||
|
||||
sign:
|
||||
needs: [pick, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# 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@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci/sign_release.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
pattern: horizon-*
|
||||
merge-multiple: true
|
||||
- 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' || '' }}
|
||||
run: |
|
||||
bash tools/ci/sign_release.sh nightly dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
|
||||
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signature
|
||||
path: dist/SHA256SUMS*
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: needs.pick.outputs.dry_run == 'true'
|
||||
with:
|
||||
name: ephemeral.pub
|
||||
path: dist/ephemeral.pub
|
||||
retention-days: 3
|
||||
|
||||
publish:
|
||||
needs: [pick, build, sign]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The signature, checked as launchers check it
|
||||
env:
|
||||
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
run: |
|
||||
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
|
||||
cargo run --locked --quiet -p horizon-release-sign -- verify --tag nightly --publishing "${key[@]}" dist
|
||||
- name: The smoke test drew a world
|
||||
# No release-blocker check: a nightly is how blockers get tested.
|
||||
run: python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json
|
||||
- name: Publish as the `nightly` pre-release
|
||||
if: needs.pick.outputs.dry_run != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SHA: ${{ needs.pick.outputs.sha }}
|
||||
DATE: ${{ needs.pick.outputs.date }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 tools/ci/nightly.py rotate
|
||||
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
|
||||
# A pre-release, never GitHub's latest: launchers on the release channel never see it.
|
||||
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#Nightly game files: Linux (horizon-linux-x86_64.tar.gz)" \
|
||||
"dist/horizon-windows-x64.zip#Nightly game files: Windows (horizon-windows-x64.zip)" \
|
||||
"dist/horizon-macos-arm64.zip#Nightly game files: macOS (horizon-macos-arm64.zip)" \
|
||||
"dist/horizon-server-linux-x86_64.tar.gz#Nightly dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
|
||||
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
|
||||
python3 tools/ci/nightly.py prune --days 7
|
||||
- name: Dry run, the nightly that would be published
|
||||
if: needs.pick.outputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "Would publish nightly at ${{ needs.pick.outputs.sha }} (${{ needs.pick.outputs.source }})"
|
||||
ls -l dist
|
||||
|
||||
answer:
|
||||
# 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'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE: ${{ inputs.issue }}
|
||||
BUILD: ${{ needs.pick.outputs.build }}
|
||||
PUBLISHED: ${{ needs.publish.result }}
|
||||
DRY_RUN: ${{ needs.pick.outputs.dry_run }}
|
||||
NAME: Nightly ${{ needs.pick.outputs.date }} · ${{ needs.pick.outputs.short }}
|
||||
RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
if [[ "$BUILD" != true ]]; then
|
||||
text="nothing new: the published Nightly is already this build."
|
||||
elif [[ "$PUBLISHED" == success && "$DRY_RUN" == true ]]; then
|
||||
text="the dry run built and signed $NAME; nothing was published. $RUN"
|
||||
elif [[ "$PUBLISHED" == success ]]; then
|
||||
text="published $NAME: https://github.com/$GITHUB_REPOSITORY/releases/tag/nightly. Launchers with Nightly on install it at their next start."
|
||||
else
|
||||
text="the build failed, nothing was published: $RUN"
|
||||
fi
|
||||
gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE/comments" -f body="🤖 Nightly: $text"
|
||||
@@ -1,46 +0,0 @@
|
||||
name: nightly command
|
||||
# `/nightly …` comments: the trusted reviewers in .github/nightly-reviewers.txt (and Ludovic) pick the pull requests
|
||||
# the Nightly holds (docs/release-flow.md "Nightly channel", tools/ci/nightly_set.py). This job only checks who asked,
|
||||
# saves the set on the nightly-state branch, starts `nightly build` and answers in the thread.
|
||||
#
|
||||
# Why no pull request's code runs here, though the token can write:
|
||||
# - `issue_comment` always runs this file from main, whoever comments and wherever; a pull request's own copy of it
|
||||
# is never read.
|
||||
# - It checks out main and runs main's nightly_set.py. The comment's text and author reach it as env vars, never in
|
||||
# a shell line.
|
||||
# - It never checks out, fetches or merges a pull request: it reads their file lists through the API. The merge
|
||||
# happens in nightly-build.yml's `pick`, with git objects only; their code runs in its `build`, without secrets.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
command:
|
||||
# Every other comment skips here, before a runner starts.
|
||||
if: startsWith(github.event.comment.body, '/nightly') && github.event.comment.user.type != 'Bot'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: write # the nightly-state branch (main is protected)
|
||||
actions: write # starts `nightly build`
|
||||
issues: write # answers in the thread
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.github/CODEOWNERS
|
||||
.github/nightly-reviewers.txt
|
||||
tools/ci/nightly_set.py
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: Run the command
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BODY: ${{ github.event.comment.body }}
|
||||
AUTHOR: ${{ github.event.comment.user.login }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
COMMENT_URL: ${{ github.event.comment.html_url }}
|
||||
run: python3 tools/ci/nightly_set.py command
|
||||
@@ -8,7 +8,7 @@ name: nightly audit
|
||||
# gh workflow run "nightly audit" -f dry_run=true # the audit, without touching issues
|
||||
on:
|
||||
schedule:
|
||||
# GitHub's schedules are in UTC: both, and the first step keeps the one that is 03:07 in Paris today.
|
||||
# GitHub's schedules are in UTC: both, and the first step keeps the one that is 03:00 in Paris.
|
||||
- cron: '7 1 * * *'
|
||||
- cron: '7 2 * * *'
|
||||
workflow_dispatch:
|
||||
@@ -23,27 +23,17 @@ permissions:
|
||||
concurrency:
|
||||
group: nightly-audit
|
||||
cancel-in-progress: false
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
# The Mac mini's runners only: the audit compares with the previous one, so it stays on one platform.
|
||||
runs-on: [self-hosted, horizon-game, macOS]
|
||||
timeout-minutes: 240 # every harness (~80 min) plus lan_integration (~40 min an attempt under load, retried)
|
||||
runs-on: [self-hosted, horizon-game]
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- name: 03:07 in Paris
|
||||
- name: 03:00 in Paris
|
||||
id: when
|
||||
env:
|
||||
SCHEDULE: ${{ github.event.schedule }}
|
||||
run: |
|
||||
# Which cron is 03:07 in Paris, from Paris's offset today: GitHub can start a schedule hours late, so the
|
||||
# clock at start time can't tell (on 2026-10-26 both started after 04:00 Paris and both skipped).
|
||||
offset=$(TZ=Europe/Paris date +%z)
|
||||
ours=$([ "$offset" = +0200 ] && echo '7 1 * * *' || echo '7 2 * * *')
|
||||
if [[ "${{ github.event_name }}" == schedule && "$SCHEDULE" != "$ours" ]]; then
|
||||
echo "Paris is at UTC${offset} today: the '$ours' schedule runs the audit."
|
||||
hour=$(TZ=Europe/Paris date +%H)
|
||||
if [[ "${{ github.event_name }}" == schedule && "$hour" != 03 ]]; then
|
||||
echo "It is ${hour}:00 in Paris: the other schedule runs the audit."
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -71,8 +61,7 @@ jobs:
|
||||
run: |
|
||||
echo "since=$(date +%Y-%m-%dT%H:%M:%S)" >> "$GITHUB_OUTPUT"
|
||||
set +e
|
||||
# lan_integration (the whole lan_e2e suite) takes ~2,400 s under load, past the usual 1,800.
|
||||
python3 tools/parity.py --all --timeout 3000 2>&1 | tee "$RUNNER_TEMP/audit.log"
|
||||
python3 tools/parity.py --all --timeout 1800 2>&1 | tee "$RUNNER_TEMP/audit.log"
|
||||
echo "exit=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
|
||||
{ echo '```'; tail -40 "$RUNNER_TEMP/audit.log"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Issues for the harnesses that regressed
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
name: parity-main
|
||||
# The vanilla harnesses on main, a batch of merges at a time (docs/release-flow.md#parity-on-main), on the Mac mini's
|
||||
# runner. One run at a time: merges that land while a run goes on wait in one pending run (GitHub keeps only the
|
||||
# newest), which checks every merge since main's last green parity-main (tools/parity_main.py base). On a failure it
|
||||
# bisects the failing harnesses over those merges and comments on the pull request that broke them. Releases ship
|
||||
# only a commit whose parity-main passed (tools/release_train.py), so nothing a harness catches reaches players.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
# The comment on the merge that broke a harness.
|
||||
pull-requests: write
|
||||
# tools/parity_main.py base reads earlier runs.
|
||||
actions: read
|
||||
concurrency:
|
||||
group: parity-main
|
||||
cancel-in-progress: false
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
parity-main:
|
||||
# A Mac mini runner of its own (ludovics-mac-mini-2 holds only this label since 2026-10-26): behind PR parity jobs
|
||||
# this run waited 2 h while every merge replaced the pending one, and on the release runner a release then waited
|
||||
# behind the next run. PR parity jobs have ludovics-mac-mini and playhorizon. Not playhorizon: its 4 CPUs
|
||||
# make a core-crate run several times longer, and a failure here bisects and comments on merges.
|
||||
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@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Tools from the runner
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Vanilla data, and resources prepared by this preparer
|
||||
run: bash tools/ci/vanilla-data.sh
|
||||
- name: Main's last green parity-main
|
||||
id: base
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: echo "sha=$(python3 tools/parity_main.py base)" >> "$GITHUB_OUTPUT"
|
||||
- name: The harnesses the merges since then can affect
|
||||
id: harnesses
|
||||
# A failure here is judged by the next step: the run passes when every failed harness passes again at HEAD.
|
||||
continue-on-error: true
|
||||
env:
|
||||
PARITY_HOSTS: ''
|
||||
BASE_SHA: ${{ steps.base.outputs.sha }}
|
||||
run: |
|
||||
echo "Merges since ${BASE_SHA}:"
|
||||
git log --first-parent --format=' %h %s' "$BASE_SHA..HEAD"
|
||||
python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400
|
||||
- name: A flake, or the merge that broke them
|
||||
if: steps.harnesses.outcome == 'failure'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PARITY_HOSTS: ''
|
||||
BASE_SHA: ${{ steps.base.outputs.sha }}
|
||||
run: |
|
||||
rm -rf .local/parity-main-logs && cp -R .local/parity-runner/logs .local/parity-main-logs 2>/dev/null || true
|
||||
python3 tools/parity_main.py bisect "$BASE_SHA" --comment
|
||||
- name: Logs of the failed harnesses
|
||||
if: steps.harnesses.outcome == 'failure'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: parity-main-logs
|
||||
path: .local/parity-main-logs/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
@@ -1,97 +0,0 @@
|
||||
name: parity
|
||||
# The vanilla harnesses a pull request's changes can affect, on the Mac mini's runner or playhorizon's, which hold the
|
||||
# maintainer's own Minecraft files (Horizon never downloads the game). Called by ci.yml for pull requests from this repository
|
||||
# and by run-parity.yml when a maintainer labels a fork's pull request run-parity. See docs/release-flow.md.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base:
|
||||
description: The pull request's base commit (the harnesses its changes since then can affect)
|
||||
type: string
|
||||
required: true
|
||||
head:
|
||||
description: The commit to check out and test
|
||||
type: string
|
||||
required: true
|
||||
full:
|
||||
description: >-
|
||||
The pull request carries the full-parity label (a change every harness reaches, such as a crate split): it
|
||||
runs on playhorizon's runner only, so the Mac mini's runners stay free for the others. While that runner is
|
||||
busy the job waits for it.
|
||||
type: boolean
|
||||
default: false
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
parity:
|
||||
runs-on: ${{ inputs.full && fromJSON('["self-hosted", "horizon-game", "playhorizon"]') || fromJSON('["self-hosted", "horizon-game"]') }}
|
||||
# A change to core crates reruns ~350 harnesses: ~60-90 min on the Mac mini; on playhorizon (4 of its 8 vCPUs, 4
|
||||
# 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@v4
|
||||
with:
|
||||
# The commit the event names: a fork's later pushes need another run-parity label. The next step merges
|
||||
# main into it.
|
||||
ref: ${{ inputs.head }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
|
||||
clean: false
|
||||
- name: The head merged into main (what merging it would land)
|
||||
id: merge
|
||||
env:
|
||||
BASE_SHA: ${{ inputs.base }}
|
||||
HEAD_SHA: ${{ inputs.head }}
|
||||
run: |
|
||||
# Harness fixes and game fixes that reached main since the branch was made then apply here too: a head
|
||||
# tested alone failed harnesses already fixed on main (server_knockback before #2114, mob_ai's heap before
|
||||
# #2270, save_roundtrip before #2212). Selection then compares with this main, so it sees only the pull
|
||||
# request's own changes. A head that conflicts with main is tested alone.
|
||||
set -euo pipefail
|
||||
git merge --abort 2>/dev/null || true
|
||||
git fetch -q --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
main=$(git rev-parse origin/main)
|
||||
if git -c user.name=parity -c user.email=parity@localhost merge -q --no-edit --no-ff "$main" >/dev/null 2>&1; then
|
||||
echo "base=$main" >> "$GITHUB_OUTPUT"
|
||||
echo "Tested: head ${HEAD_SHA:0:9} merged into main ${main:0:9}" | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
git merge --abort
|
||||
echo "base=$BASE_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::head ${HEAD_SHA:0:9} conflicts with main ${main:0:9}: tested alone, without main's fixes"
|
||||
echo "Tested: head ${HEAD_SHA:0:9} alone (it conflicts with main ${main:0:9})" | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Tools from the runner
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Vanilla data, and resources prepared by this preparer
|
||||
run: bash tools/ci/vanilla-data.sh
|
||||
- name: The prepared output is the one the preparer's OUTPUT_VERSION says
|
||||
env:
|
||||
BASE_SHA: ${{ steps.merge.outputs.base }}
|
||||
run: |
|
||||
# A head made before OUTPUT_VERSION (#1947) that conflicts with main has neither the tool nor the number.
|
||||
if [ ! -f tools/prepare_output.py ] || ! grep -q OUTPUT_VERSION crates/horizon-prepare/src/lib.rs; then
|
||||
echo "This head's preparer has no OUTPUT_VERSION: nothing to check"
|
||||
exit 0
|
||||
fi
|
||||
python3 tools/prepare_output.py check --cache --base "$BASE_SHA"
|
||||
- name: The version constants are the pinned jar's
|
||||
run: python3 tools/gen_version_consts.py --check
|
||||
- name: The harnesses this change can affect
|
||||
env:
|
||||
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: Logs of the failed harnesses
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: parity-logs
|
||||
path: .local/parity-runner/logs/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
+120
-96
@@ -1,26 +1,17 @@
|
||||
name: release
|
||||
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
|
||||
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides,
|
||||
# for a commit whose checks and parity-main (the vanilla harnesses on main) both passed:
|
||||
# a release when 5 pull requests merged since the last one changed what players run and the newest merged 5 minutes
|
||||
# ago, or when the oldest has waited 6 hours, or at once for a pull request labelled release-now.
|
||||
# RELEASES_PAUSED=1 (repository variable) stops them; so does an open issue labelled release-blocker, forced runs too.
|
||||
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides:
|
||||
# a release when pull requests merged since the last one changed what players run and the newest merged 15 minutes
|
||||
# ago (the oldest an hour ago, if merges never pause), or at once for a pull request labelled release-now.
|
||||
# RELEASES_PAUSED=1 (repository variable) stops them.
|
||||
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
|
||||
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
|
||||
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
|
||||
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as the release and GitHub's latest: one
|
||||
# channel, which launchers, `horizon-server update` and install.sh follow at once. So it is published only after the
|
||||
# checks that used to gate a promotion pass in this run: the Mac mini's smoke test drew a world, and no release-blocker
|
||||
# is open (tools/release_train.py gate); otherwise nothing is published. macOS: with the repository variable
|
||||
# MACOS_SIGN_IDENTITY set, the programs are signed with
|
||||
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
|
||||
# (packaging/macos/README.md); unset (forks), ad hoc.
|
||||
# publishes vYYYY.MM.DD[.N] with notes from the merged pull requests.
|
||||
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
|
||||
# gh workflow run release -f dry_run=false -f force=true # on main: release now, whatever the rules say
|
||||
on:
|
||||
workflow_run:
|
||||
# A game release ships a commit both passed on (parity-main runs on batches: the newest commit it passed on).
|
||||
workflows: [checks, parity-main]
|
||||
workflows: [checks]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
schedule:
|
||||
@@ -37,13 +28,8 @@ on:
|
||||
default: false
|
||||
permissions:
|
||||
contents: read
|
||||
# The open release-blocker issues (decide).
|
||||
issues: read
|
||||
concurrency:
|
||||
# One release at a time. A hand-started dry run publishes nothing, so it can't race a real release: it gets a
|
||||
# group of its own. In the shared group, a release queued behind it replaced it as the pending run and GitHub
|
||||
# cancelled it (#2779's dry run, 2026-10-27), which happens all day while releases go out in batches.
|
||||
group: game-release${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '-dry-run' || '' }}
|
||||
group: game-release
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
decide:
|
||||
@@ -55,7 +41,6 @@ jobs:
|
||||
outputs:
|
||||
release: ${{ steps.decide.outputs.release }}
|
||||
tag: ${{ steps.decide.outputs.tag }}
|
||||
version: ${{ steps.decide.outputs.version }}
|
||||
head: ${{ steps.decide.outputs.head }}
|
||||
dry_run: ${{ steps.mode.outputs.dry_run }}
|
||||
steps:
|
||||
@@ -69,77 +54,142 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASES_PAUSED: ${{ vars.RELEASES_PAUSED }}
|
||||
# The commit whose checks or parity-main just passed; a dry run builds the branch it was started on;
|
||||
# otherwise main's newest commit with green checks and parity-main.
|
||||
# The commit whose checks just passed; a dry run builds the branch it was started on; otherwise
|
||||
# main's newest commit with green checks.
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha || (steps.mode.outputs.dry_run == 'true' && github.sha) || '' }}
|
||||
FORCE: ${{ inputs.force && 'yes' || '' }}
|
||||
run: |
|
||||
python3 tools/release_train.py decide --kind game ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
|
||||
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
|
||||
# The version the programs are built with (docs/versioning.md): the tag without its v.
|
||||
tag=$(jq -r '.tag' decision.json)
|
||||
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
|
||||
# A dry run builds whatever the decision, to test the builds.
|
||||
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then echo "release=true" >> "$GITHUB_OUTPUT"; fi
|
||||
|
||||
build:
|
||||
needs: decide
|
||||
if: needs.decide.outputs.release == 'true'
|
||||
# Every platform's archive, and macOS's smoke test (game-build.yml).
|
||||
uses: ./.github/workflows/game-build.yml
|
||||
with:
|
||||
ref: ${{ needs.decide.outputs.head }}
|
||||
version: ${{ needs.decide.outputs.version }}
|
||||
trusted: true # main: the release runner, the release cache, no shared sccache
|
||||
|
||||
sign:
|
||||
needs: [decide, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
|
||||
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
|
||||
# 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' || '' }}
|
||||
name: build ${{ matrix.name }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: linux
|
||||
os: ubuntu-22.04
|
||||
dir: horizon-linux-x86_64
|
||||
asset: horizon-linux-x86_64.tar.gz
|
||||
- name: windows
|
||||
os: windows-latest
|
||||
dir: horizon-windows-x64
|
||||
asset: horizon-windows-x64.zip
|
||||
exe: .exe
|
||||
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
|
||||
rustflags: -C target-feature=+crt-static
|
||||
- name: macos
|
||||
mac: true
|
||||
dir: horizon-macos-arm64
|
||||
asset: horizon-macos-arm64.zip
|
||||
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci/sign_release.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
ref: ${{ needs.decide.outputs.head }}
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
# On the Mac mini's runner, keep target/ (incremental) and .local/ (the prepared resources).
|
||||
clean: ${{ runner.environment == 'github-hosted' }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
path: dist
|
||||
pattern: horizon-*
|
||||
merge-multiple: true
|
||||
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
|
||||
env:
|
||||
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
|
||||
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
|
||||
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
TAG: ${{ needs.decide.outputs.tag }}
|
||||
shared-key: release-${{ matrix.name }}
|
||||
- name: ALSA and udev headers
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
|
||||
run: cargo build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
|
||||
- name: Every program the release folder takes was built
|
||||
run: |
|
||||
bash tools/ci/sign_release.sh "$TAG" dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
|
||||
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
|
||||
done
|
||||
- name: Only system libraries
|
||||
# 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: |
|
||||
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
||||
file="target/release/$binary${{ matrix.exe }}"
|
||||
if [[ "$RUNNER_OS" == macOS ]]; then
|
||||
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
|
||||
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
|
||||
else
|
||||
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
|
||||
fi
|
||||
done
|
||||
- name: Every program starts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bin=target/release
|
||||
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
|
||||
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
|
||||
"$bin/horizon-prepare${{ matrix.exe }}" --version
|
||||
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
|
||||
"$bin/horizon-server${{ matrix.exe }}" --version
|
||||
- name: Tools from the runner
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Resources prepared from the runner's own Minecraft
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: bash tools/ci/vanilla-data.sh
|
||||
- name: The game runs (offscreen, a new world, 1500 frames)
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
|
||||
home="$RUNNER_TEMP/horizon-smoke"
|
||||
rm -rf "$home" && mkdir -p "$home"
|
||||
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
|
||||
client="$PWD/target/release/horizon-client"
|
||||
resources="$PWD/.local/horizon-client/$version"
|
||||
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
|
||||
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
|
||||
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
|
||||
python3 - "$home/smoke.json" <<'EOF'
|
||||
import json, sys
|
||||
report = json.load(open(sys.argv[1]))
|
||||
print(json.dumps(report))
|
||||
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
|
||||
or not report.get("meshed_sections") or not report.get("player"):
|
||||
sys.exit("the smoke run did not draw a world")
|
||||
EOF
|
||||
- name: Package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo run --locked -p xtask -- package "target/release/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
|
||||
cd dist
|
||||
rm -f "${{ matrix.asset }}"
|
||||
case "${{ matrix.asset }}" in
|
||||
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
|
||||
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
|
||||
else ditto -c -k --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"; fi ;;
|
||||
esac
|
||||
ls -l "${{ matrix.asset }}"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signature
|
||||
path: dist/SHA256SUMS*
|
||||
name: ${{ matrix.asset }}
|
||||
path: dist/${{ matrix.asset }}
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: needs.decide.outputs.dry_run == 'true'
|
||||
with:
|
||||
name: ephemeral.pub
|
||||
path: dist/ephemeral.pub
|
||||
retention-days: 3
|
||||
|
||||
publish:
|
||||
needs: [decide, build, sign]
|
||||
needs: [decide, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
issues: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -155,45 +205,19 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The signature, checked as launchers check it
|
||||
env:
|
||||
TAG: ${{ needs.decide.outputs.tag }}
|
||||
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
|
||||
run: |
|
||||
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
|
||||
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
|
||||
- name: Ready to publish (the smoke test passed, no release-blocker is open)
|
||||
# Right before publishing: an issue labelled while the builds ran still holds this release. A dry run only
|
||||
# reports what a real run would do.
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
DRY_RUN: ${{ needs.decide.outputs.dry_run }}
|
||||
run: |
|
||||
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
|
||||
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
|
||||
echo "::warning::A real run would publish nothing"
|
||||
fi
|
||||
- name: Publish ${{ needs.decide.outputs.tag }}
|
||||
if: needs.decide.outputs.dry_run != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ needs.decide.outputs.tag }}
|
||||
run: |
|
||||
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
|
||||
# The release and the latest at once: there is one channel (docs/release-flow.md). Each archive gets a
|
||||
# label (file#label), which GitHub's page shows instead of the file name, so nobody takes the game's
|
||||
# files for the download (#1647). Launchers, servers and install.sh match the name, which stays.
|
||||
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
|
||||
--notes-file notes/body.md --latest \
|
||||
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs: Linux (horizon-linux-x86_64.tar.gz)" \
|
||||
"dist/horizon-windows-x64.zip#Game files the launcher installs: Windows (horizon-windows-x64.zip)" \
|
||||
"dist/horizon-macos-arm64.zip#Game files the launcher installs: macOS (horizon-macos-arm64.zip)" \
|
||||
"dist/horizon-server-linux-x86_64.tar.gz#Dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
|
||||
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
|
||||
--notes-file notes/body.md --latest dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
|
||||
dist/horizon-macos-arm64.zip
|
||||
- name: Dry run, the release that would be published
|
||||
if: needs.decide.outputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }} as the release and latest: $(cat notes/title.txt)"
|
||||
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }}: $(cat notes/title.txt)"
|
||||
ls -l dist
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
name: run-parity
|
||||
# A fork's pull request runs the parity job (parity.yml) on the Mac mini only when a maintainer who has read the
|
||||
# change adds the run-parity label: once, on the commit the pull request has then. Remove the label and add it
|
||||
# again after new pushes. A separate workflow, so that labels never start `checks`: there a label's run reported a
|
||||
# skipped `verify`, and GitHub counts a skipped required check as passed.
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: run-parity-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
parity:
|
||||
if: github.event.label.name == 'run-parity'
|
||||
uses: ./.github/workflows/parity.yml
|
||||
with:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
|
||||
@@ -31,6 +31,3 @@
|
||||
/.horizon-cache/
|
||||
/crash-reports/
|
||||
/debug/
|
||||
|
||||
# Release-only compiled original exporter bundle.
|
||||
/exporters/
|
||||
@@ -1,184 +0,0 @@
|
||||
# Horizon: a map for agents
|
||||
|
||||
Minecraft: Java Edition 26.3 rewritten in Rust (wgpu). Vanilla is the specification wherever it has the
|
||||
feature. Read [CONTRIBUTING.md](CONTRIBUTING.md) once; this page tells you where things are.
|
||||
Feature → files → harness for the most-changed areas: [docs/architecture.md](docs/architecture.md).
|
||||
|
||||
## Rules that bite
|
||||
|
||||
- **Never commit Mojang's files**: game jars, assets, decompiled source, prepared resources (`.local/`).
|
||||
`tools/check_public_tree.py` rejects them; `git config core.hooksPath .githooks` runs it on commit.
|
||||
- **Keep your runs away from real player data.** Any game or launcher you start gets fresh `mktemp -d`
|
||||
folders in `HORIZON_HOME` (the game's data), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
|
||||
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`, 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 whose agent opened it and with what.
|
||||
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
|
||||
contributor's agent names its own person and tool (issues: "Filed by", reviews: "Review by"). Every
|
||||
comment starts with `🤖 Agent:`. Never write as Ludovic or as anyone else.
|
||||
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
|
||||
with that agent's own "Generated with" line. Ludovic's agents use
|
||||
`Co-Authored-By: Claude <model> <[email protected]>` and
|
||||
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
|
||||
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
|
||||
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
|
||||
they would change the vanilla look.
|
||||
- Small PRs (< ~500 lines). Labels: `feature` (players get something new), `skip-release` (they see no
|
||||
change), none for a fix. Agents never add `release-now`: the maintainer decides it. Merges ship in batches of 5, or after
|
||||
6 hours ([docs/release-flow.md](docs/release-flow.md)).
|
||||
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
|
||||
|
||||
## Crates
|
||||
|
||||
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
|
||||
|
||||
| Crate | Owns |
|
||||
|---|---|
|
||||
| `horizon-client` | The game: a lib (`src/lib.rs`, ~170 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
|
||||
| `horizon-items` | Blocks and items below gameplay: `blocks.rs` (block facts), `items.rs`, `item_data.rs` (components), `enchant.rs`, `cooldowns.rs`, `bundle.rs`, `item_nbt.rs`. Re-exported as `horizon_client::items`... |
|
||||
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
|
||||
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
|
||||
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
|
||||
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
|
||||
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
|
||||
| `horizon-assets` | Resource decoding: block-state and item models, baking, atlases |
|
||||
| `horizon-prepare` | Builds `.local/horizon-client/26.3` from the player's own Minecraft install (runs the Java exporters) |
|
||||
| `horizon-server` | `horizon-server`: `server.toml`, CLI, supervisor of the headless game ([docs/hosting.md](docs/hosting.md)) |
|
||||
| `horizon-server-files` | `server.properties`, eula, ops/whitelist/bans, usercache |
|
||||
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
|
||||
| `horizon-mods`, `horizon-mod-sdk`, `cargo-horizon-mod` | Sandboxed WebAssembly mods: runtime, SDK + WIT, `cargo horizon-mod` ([docs/mods.md](docs/mods.md)) |
|
||||
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
|
||||
| `horizon-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 |
|
||||
| `horizon-telemetry`, `horizon-telemetry-server`, `horizon-bench` | Opt-in data collection, its ingest server, the benchmark report format |
|
||||
| `horizon-dualsense`, `horizon-threads`, `horizon-release-sign`, `horizon-bots`, `xtask` | DualSense output, thread priorities, signed release checksums, load-test bots, repo tasks |
|
||||
| `launcher/` | GPUI launcher: `main.rs` (UI), `backend.rs` (installs, updates), `cli.rs`, `mcp.rs`, `*_page.rs` |
|
||||
|
||||
Other top-level folders: `tools/` (harnesses and dev scripts), `tests/` (Python unit tests: `python3 -m
|
||||
unittest discover -s tests`), `mods/horizon/src/test/java/dev/horizonlab/parity/` (Java oracles),
|
||||
`docs/`, `.github/workflows/`. The website (playhorizon.dev) is the private ludovic111/horizon-site: its build
|
||||
reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` and `crates/horizon-theme/tokens.css` from main.
|
||||
|
||||
## Build and test fast
|
||||
|
||||
```sh
|
||||
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
|
||||
python3 tools/pr_ready.py --quick # same without harnesses
|
||||
cargo test -p horizon-client --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
|
||||
cargo test -p horizon-client --features horizon-client/mods --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
|
||||
python3 tools/parity.py mob_ai sculk # named harnesses
|
||||
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host); --in-frame compares the fallback
|
||||
python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
|
||||
```
|
||||
|
||||
- Harnesses and the game need the vanilla data in `.local/` (`minecraft/`, `horizon-client/26.3`, `datagen/`,
|
||||
`tools/`): see [docs/parity-workflow.md](docs/parity-workflow.md). Without it, pr_ready lists the
|
||||
harnesses for CI.
|
||||
- A harness = `tools/test_<name>_parity.py` (324 of them) + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
|
||||
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (horizon-world 175, horizon-client 104),
|
||||
or the game binary with a fixture env var (`game/dispense_fixture.rs`, `game/sculk_fixture.rs`,
|
||||
`game/command_harness.rs`, `game/net_harness.rs`). Copy a nearby harness for a new one.
|
||||
- Probes build into `.local/parity-target/`; harnesses find binaries via `lab.cargo_target()`, never `target/`.
|
||||
- What a change reruns (of 329 harnesses, measured 2026-10-23): a file of the game binary (`game.rs`, `game/`)
|
||||
~25, a file of the client lib ~103, of `horizon-world` ~103–109, of `horizon-core` ~119. Plan for it.
|
||||
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
|
||||
- **macOS:** `cargo build/test --workspace` may fail to link the two wasm guest cdylibs (undefined
|
||||
`horizon:mods/events` symbols) until #1446 lands: add `--exclude coords-hud --exclude predator-missile --exclude horizon-mods-test-guest --exclude horizon-mods-test-ui-guest`.
|
||||
Their real check is `cargo test -p horizon-mods` (builds them for `wasm32-unknown-unknown`).
|
||||
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
|
||||
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
|
||||
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
|
||||
then `horizon-ctl --socket … state|keys|turn|command|screenshot|quit`
|
||||
([docs/horizon-ctl-commands.md](docs/horizon-ctl-commands.md)).
|
||||
|
||||
## Vanilla reference
|
||||
|
||||
- Decompiled sources: `.local/minecraft/26.3/sources/{client,server}/` (`python3 tools/lab.py decompile`).
|
||||
- Bytecode queries (callers through lambdas, every overload):
|
||||
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`
|
||||
([docs/decompilation-status.md](docs/decompilation-status.md)).
|
||||
- A harness runs the oracle against the real 26.3 jars and the Rust probe on the same inputs, then compares
|
||||
their JSON. Oracle runs are cached by `tools/parity_shim.py`; `--fresh` reruns them.
|
||||
- Mirror vanilla's `Math` calls with `horizon_core::jmath`, and vanilla's RNG with `horizon_core::random`:
|
||||
random draws must happen in vanilla's order.
|
||||
|
||||
## horizon-client: where things live
|
||||
|
||||
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
|
||||
`Game`; bin code reaches the lib as `horizon_client::<module>`.
|
||||
|
||||
| Subsystem | Files |
|
||||
|---|---|
|
||||
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `window.rs`, `spans.rs` |
|
||||
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
|
||||
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
|
||||
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
|
||||
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
|
||||
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
|
||||
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
|
||||
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; 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`, `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs` |
|
||||
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
|
||||
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
|
||||
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
|
||||
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server` |
|
||||
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `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`; L: `keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `osk.rs`, `clipboard.rs`; G: `pad.rs`, `pad_menu.rs` |
|
||||
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
|
||||
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`; `horizon-client-core/src/lang.rs`; crate `horizon-assets`; G: `packs.rs` |
|
||||
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
|
||||
| Mods | crate `horizon-mods`; G: `mods.rs`; L: `mod_content.rs`, `mod_list.rs` |
|
||||
| Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs`, `tweaks.rs`, `options.rs` |
|
||||
|
||||
## How `Game` is organised
|
||||
|
||||
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
|
||||
the core (tick, render, input, inventory, world open/close) and each `game/<feature>.rs` adds an
|
||||
`impl Game<'_>` block for one feature, with its own state types. Fields are private to the bin.
|
||||
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
|
||||
the file is usually `game/<thing>s.rs`. Who calls it: grep the name in game.rs's `fn tick` / `fn tick_world`.
|
||||
- Fields that say which mode runs: `remote` (this window is a client of a server: multiplayer, LAN guest, or
|
||||
single player on its server thread), `lan` (`LanHost`: this world is open to the network), `integrated`
|
||||
(the window's own server thread), `serving` (this `Game` *is* that server thread), `options.dedicated`.
|
||||
`fn tick` returns `tick_remote()` when `remote` is set; nothing else of the local simulation runs.
|
||||
- A server's players run through the same gameplay code as the local player: `as_guest` swaps the guest's
|
||||
state (position, inventory, mode, mining, item use) into `Game` for one call (`game/lan_actions.rs`), and
|
||||
`levels.rs` swaps whole dimensions in the same way. Changes reach clients through the block log, level
|
||||
events and the entity tracker (`lan/tracking.rs`, `game/lan_entities.rs`).
|
||||
- Shared read-only data: `Resources` (`res`: blocks, items, recipes, mob types, biomes) and `PackResources`
|
||||
(`pack`), both in game.rs.
|
||||
|
||||
## The two single-player paths
|
||||
|
||||
1. **Server thread (default since #1514):** `game/integrated.rs` runs a second `Game` (`serving`) on a
|
||||
"Server thread" with one level per dimension; the window joins it as a client through memory (`remote`),
|
||||
exactly as a LAN guest or a dedicated server's player does. This is what players run. Open
|
||||
`[server-thread]` issues list the gaps left.
|
||||
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
|
||||
the world itself, one level that follows the player (`fn enter_dimension`). Deterministic rule fixtures still use this path; player-facing integration uses the default server thread.
|
||||
|
||||
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
|
||||
when you touch player-facing gameplay:
|
||||
|
||||
```sh
|
||||
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
|
||||
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
|
||||
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
|
||||
python3 tools/lan_e2e.py --in-frame --check <name> # comparison (portals, levels, distance, lava need server-thread mode)
|
||||
python3 tools/parity.py dedicated_server server_entities server_player_damage
|
||||
```
|
||||
|
||||
## Docs worth knowing
|
||||
|
||||
[docs/parity-workflow.md](docs/parity-workflow.md) (runner, gate, shards, scopes) ·
|
||||
[docs/native-client.md](docs/native-client.md) (options, controls, horizon-ctl) ·
|
||||
[docs/server-levels.md](docs/server-levels.md) (levels per dimension) ·
|
||||
[docs/rust-parity.md](docs/rust-parity.md) (what is ported) · [docs/known-issues.md](docs/known-issues.md) ·
|
||||
[docs/release-flow.md](docs/release-flow.md) · [docs/README.md](docs/README.md) (index).
|
||||
+2
-24
@@ -4,13 +4,8 @@ Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, a
|
||||
everything is that **it plays exactly like vanilla**. This page is the short version of how we work; questions are
|
||||
welcome on [Discord](https://discord.gg/HcTeMZxe5E).
|
||||
|
||||
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.
|
||||
|
||||
**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
|
||||
suggest something. A pull request for a roadmap item carries its `roadmap:<id>` label; ideas go to the suggestion
|
||||
queue (the hub, or `/suggest` on Discord) rather than to issues, which are for bugs.
|
||||
This repository holds the game, the launcher and the website, plus the tools, tests and docs that build, check and
|
||||
release them. Anything else (videos, Discord and other community material) lives elsewhere.
|
||||
|
||||
## Build it
|
||||
|
||||
@@ -44,17 +39,6 @@ probe against Horizon, and compares their outputs. There are about 290 of them.
|
||||
|
||||
Never commit game files, decompiled source or prepared resources (`.local/`): `tools/check_public_tree.py` rejects
|
||||
them (`git config core.hooksPath .githooks` runs it before every commit).
|
||||
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
|
||||
|
||||
Horizon's code is written from Minecraft 26.3 itself and from public documentation, never from another mod, loader
|
||||
or renderer. Don't read, copy, translate or paraphrase the source of Iris, OptiFine, Sodium or any other project,
|
||||
and don't send code an AI tool wrote from it. Their user documentation and file formats are fine to read, and their
|
||||
packs and mods are fine to run and observe. Shader packs were removed on 24 October 2026 (#1561) because their code
|
||||
had been written from another project's source; they come back only as a clean-room rewrite from a written
|
||||
description of the pack format.
|
||||
|
||||
## Before asking for review
|
||||
|
||||
@@ -69,11 +53,6 @@ crates you touched (and the launcher's), generated files, `Cargo.lock`, the Pyth
|
||||
your change can affect. Without the vanilla data on your machine it lists those harnesses instead; CI runs them.
|
||||
`--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
|
||||
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- **Small.** One change per pull request, under about 500 lines of code where you can; split bigger work into
|
||||
@@ -87,7 +66,6 @@ configuration and toolchain changes select the whole workspace; CI also keeps it
|
||||
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
|
||||
|
||||
Every merge to main is released automatically ([release flow](docs/release-flow.md)), so main always stays playable.
|
||||
How agents, checks, releases and signed updates keep this safe: [security](docs/security.md).
|
||||
|
||||
## Parity checks on forks
|
||||
|
||||
|
||||
Generated
+672
-544
File diff suppressed because it is too large.
Load diff
+2
-51
@@ -1,47 +1,6 @@
|
||||
[workspace]
|
||||
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal"]
|
||||
members = [
|
||||
"crates/horizon-core",
|
||||
"crates/horizon-dualsense",
|
||||
"crates/horizon-threads",
|
||||
"crates/horizon-hdr",
|
||||
"crates/horizon-world",
|
||||
"crates/horizon-client",
|
||||
"crates/horizon-client-core",
|
||||
"crates/horizon-command",
|
||||
"crates/horizon-items",
|
||||
"crates/horizon-server-files",
|
||||
"crates/horizon-server",
|
||||
"crates/horizon-bots",
|
||||
"crates/horizon-assets",
|
||||
"crates/horizon-protocol",
|
||||
"crates/horizon-theme",
|
||||
"crates/horizon-prepare",
|
||||
"crates/horizon-telemetry",
|
||||
"crates/horizon-bench",
|
||||
"crates/horizon-friends",
|
||||
"crates/horizon-relay",
|
||||
"crates/horizon-mod-sdk",
|
||||
"crates/horizon-mods",
|
||||
"crates/horizon-mods/tests/guest",
|
||||
"crates/horizon-mods/tests/ui_guest",
|
||||
"crates/cargo-horizon-mod",
|
||||
"crates/horizon-port-pack",
|
||||
"crates/horizon-shaders",
|
||||
"crates/horizon-shader-api",
|
||||
"crates/horizon-release-sign",
|
||||
"crates/horizon-mod-sdk/examples/coords-hud",
|
||||
"crates/horizon-mod-sdk/examples/predator-missile",
|
||||
"crates/horizon-mod-sdk/examples/welcome",
|
||||
"crates/horizon-mod-sdk/examples/beacons",
|
||||
"mods/official/fps-overlay",
|
||||
"mods/official/keystrokes",
|
||||
"mods/official/far-view",
|
||||
"mods/official/minimap",
|
||||
"mods/official/zoom",
|
||||
"xtask",
|
||||
"mods/official/recipes",
|
||||
]
|
||||
exclude = ["launcher", "crates/horizon-telemetry-server"]
|
||||
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
@@ -57,14 +16,6 @@ type_complexity = "allow"
|
||||
needless_range_loop = "allow"
|
||||
large_enum_variant = "allow"
|
||||
|
||||
# wgpu-hal 30.0.1 with gfx-rs/wgpu#10124: wgpu 30 creates a Vulkan descriptor
|
||||
# pool with no pool sizes while it creates the device, which Vulkan allows only
|
||||
# since 1.3.215, and Intel's Windows driver for 6th-10th gen iGPUs (HD 520/530,
|
||||
# UHD 620/630) crashes on it with an access violation (#746). Drop this and the
|
||||
# folder once a wgpu release has the fix.
|
||||
[patch.crates-io]
|
||||
wgpu-hal = { path = "third_party/wgpu-hal" }
|
||||
|
||||
# Line tables keep file:line in backtraces; full debug info made every
|
||||
# example and test binary hundreds of megabytes and slow to link.
|
||||
[profile.dev]
|
||||
|
||||
@@ -14,65 +14,104 @@
|
||||
<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>Documentation</b></a> ·
|
||||
<a href="https://github.com/ludovic111/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>
|
||||
<a href="#what-changed-in-v202610131"><b>What changed</b></a> ·
|
||||
<a href="#screenshots"><b>Screenshots</b></a> ·
|
||||
<a href="#project-status"><b>Parity status</b></a> ·
|
||||
<a href="docs/README.md"><b>Documentation</b></a>
|
||||
</p>
|
||||
|
||||
# Horizon
|
||||
|
||||
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
|
||||
A Rust rewrite of Minecraft: Java Edition 26.3 built for one-to-one vanilla parity, checked against the game itself with differential tests.
|
||||
|
||||
Every subsystem matches vanilla in all 281 parity harnesses, on Linux and macOS: 100% of what the harnesses compare (see [Project status](#project-status)). The native client is playable on Linux, macOS (Apple Silicon) and Windows.
|
||||
|
||||
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
|
||||
|
||||
## What changed in v2026.10.13.1
|
||||
|
||||
The latest release ([notes](https://github.com/ludovic111/horizon/releases/tag/v2026.10.13.1)):
|
||||
|
||||
- **Horizon's own menus:** a new title screen, world list, options and pause menu in the launcher's style, over a live frozen landscape. The vanilla look is still available: Options → Interface → Menu style → Classic.
|
||||
- **Graphics settings, Simple and Advanced:** presets (Performance, Balanced, Quality, Ultra), or every rendering, quality, display and performance setting, each with a short description and its cost, plus a performance overlay.
|
||||
- **Much faster rendering:** for example, at render distance 8 over a village, 363 → 2,409 FPS on an RTX 4080. Render distance 32 still stutters in places.
|
||||
- **Fixed:** no sound on Linux; Linux builds that only ran on the machine they were built on; a missing DLL on Windows (the launcher no longer needs the Visual C++ runtime); memory growing and the frame rate dropping near villages (mobs were duplicated when chunks reloaded); resource preparation failing on Linux with "Missing … java-objc-bridge" ([#41](https://github.com/ludovic111/horizon/issues/41)); the hand and held item at high FOV; missing swing animations.
|
||||
|
||||
## Download
|
||||
|
||||
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
|
||||
Horizon is downloaded only through **the Horizon launcher**: get it from [playhorizon.dev](https://playhorizon.dev) or from [the launcher's releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true) (the newest `launcher-v*` release, currently [launcher-v0.1.3](https://github.com/ludovic111/horizon/releases/tag/launcher-v0.1.3)), for Linux x86-64, macOS Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its resources from your own copy of Minecraft, starts the client or a dedicated server, and updates itself.
|
||||
|
||||
**You need** a Microsoft account that owns Minecraft: Java Edition (the launcher signs in with it, as other launchers do; see [Microsoft account](docs/microsoft-account.md)); Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 runtime, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
|
||||
**You need** Minecraft: Java Edition installed with the official launcher, with 26.3 played once; Python 3.10 or newer and Java 25 (a JDK, e.g. Eclipse Temurin 25) for the one-time resource preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac.
|
||||
|
||||
### The launcher
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: a version ready to play, its summary, Client and Server, the version picker and Play"></td>
|
||||
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page"></td>
|
||||
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: v2026.10.14.1 ready to play, its summary, Client and Server, the version picker and Play, the resources and the Minecraft folder as a numbered list"></td>
|
||||
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page, showing the notes of Horizon 2026.10.14.1"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## What works today
|
||||
It lists Horizon's releases and installs the build for your platform, shows each release's patch notes, updates Horizon and itself, and doubles as a command-line tool and an MCP server. See [the launcher's README](launcher/README.md).
|
||||
|
||||
The [documentation](https://playhorizon.dev/docs/) covers each of these.
|
||||
## Screenshots
|
||||
|
||||
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
|
||||
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
|
||||
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
|
||||
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining servers, online-mode ones with the Microsoft account the launcher signs in with. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
|
||||
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
|
||||
Captured in the Rust client itself, with vanilla assets and no mods or shaders.
|
||||
|
||||
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
|
||||
### Menus and settings
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="2"><img src="docs/media/menu-title.webp" alt="Horizon's title screen: the mark and the pages on the left, the Horizon title, Singleplayer and Multiplayer, and the last world played and the faint wordmark, over a veiled snowy landscape"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/menu-worlds.webp" alt="The Singleplayer page: three worlds with their dates and game modes, and Create world and Play buttons"></td>
|
||||
<td width="50%"><img src="docs/media/settings.webp" alt="The Video settings in their Simple view: a preset, render distance, field of view, framerate, VSync and GUI scale, each with what it costs"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
The vanilla menus are one setting away: Options → Interface → Menu style → Classic. See [Graphics settings](docs/graphics.md).
|
||||
|
||||
### In game
|
||||
|
||||
<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>
|
||||
<td width="50%"><img src="docs/media/village.webp" alt="A plains village with farms, an iron golem and animals"></td>
|
||||
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie attacks near the village"></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>
|
||||
<td width="50%"><img src="docs/media/night.webp" alt="Moonrise over a torch-lit village and river"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/underwater.webp" alt="Salmon swimming in a river"></td>
|
||||
<td width="50%" valign="middle">
|
||||
Vanilla world generation ported to Rust (terrain, aquifers, carvers, features, villages and the Nether), every block model with smooth lighting and biome tints, the vanilla sky and fog, mobs, the HUD, chat and advancements: everything in these frames is drawn by Horizon.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Host a server
|
||||
## What it does
|
||||
|
||||
The launcher's **Servers** page creates, starts, backs up and updates a Horizon server with buttons, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and runs without Java. Guide: [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
|
||||
Horizon reimplements the Minecraft Java client and its game logic in Rust, one subsystem at a time, and compares each piece with the vanilla game. Each `tools/test_*_parity.py` harness drives the same inputs through Horizon and through the vanilla client and server bytecode from your own copy of the game, then checks that the outputs are identical. It is for players and developers interested in game reimplementation, and for anyone who wants to see how far a behaviour-exact native rewrite can go.
|
||||
|
||||
## Modpacks
|
||||
The [native client](docs/native-client.md) opens on a vanilla-style title screen with world creation and selection, and generates worlds with the Rust port of the vanilla generators: noise terrain, aquifers, surface rules, carvers and placed features, all checked against vanilla. It streams and lights chunks on worker threads and renders every vanilla block model with smooth lighting, biome tints, the vanilla sky, sun, moon, stars and clouds. Gameplay includes:
|
||||
|
||||
The launcher's **Modpacks** page imports a Modrinth or CurseForge pack, shows which of its mods Horizon can carry over, and plays the ones it can as their own profile. Today that means content mods made of data and assets; mods with Java code are not ported yet. See [Modpacks](https://playhorizon.dev/docs/modpacks).
|
||||
- validated player physics, health and hunger;
|
||||
- mining with vanilla tool rules and loot tables;
|
||||
- placing, crafting, chests and furnaces;
|
||||
- dropped items, particles and sounds;
|
||||
- passive and hostile mobs with combat and vanilla AI, including guardians, shulkers, illagers, ravagers, the wither, the ender dragon fight, the breeze, the creaking, the sniffer, the happy ghast, llamas and the copper golem;
|
||||
- villagers that take jobs, trade, restock, meet at the bell and sleep in beds, on a port of vanilla's brain;
|
||||
- raids and pillager patrols;
|
||||
- dispensers, crafters, the loom and the recipe book;
|
||||
- every command, scoreboards and teams, the player list, subtitles and the HUD overlays;
|
||||
- boats, minecarts, item frames, paintings and armor stands;
|
||||
- shields, maps, books, fireworks, beacons, advancements and statistics;
|
||||
- Anvil saves, joining vanilla servers, and opening a world to LAN.
|
||||
|
||||
## Agent control
|
||||
|
||||
@@ -108,22 +147,7 @@ cargo run --release -p horizon-client
|
||||
|
||||
Everything prepared lands in the gitignored `.local/`. It is derived from the game: never commit it or share it.
|
||||
|
||||
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, scripting through `horizon-ctl` and the architecture are described in [the native client guide](docs/native-client.md). The rest of the developer documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
What is being done now, what comes next and where you can help, in plain words and in technical detail: [ROADMAP.md](ROADMAP.md). [playhorizon.dev/hub](https://playhorizon.dev/hub) shows it with the work board, the journal of what shipped and how to help.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels), or [the contributing guide](https://playhorizon.dev/docs/contributing) on the site. Questions and ideas: [Discord](https://discord.gg/HcTeMZxe5E). Before opening a pull request, run the self-check and paste the block it prints:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
python3 tools/pr_ready.py
|
||||
```
|
||||
|
||||
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a maintainer's computer holding their own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
|
||||
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, playing and scripting through `horizon-ctl` and its MCP server, and the architecture are described in [the native client guide](docs/native-client.md). The rest of the documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
|
||||
|
||||
## Project status
|
||||
|
||||
@@ -158,7 +182,18 @@ them too (Rust uses Java's own float maths on each platform).
|
||||
|
||||
### Known issues
|
||||
|
||||
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
|
||||
Online-mode servers (Mojang's session servers) are not supported; skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels). Before opening a pull request, run the self-check and paste the block it prints:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
python3 tools/pr_ready.py
|
||||
```
|
||||
|
||||
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a self-hosted runner holding the maintainer's own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
-980
@@ -1,980 +0,0 @@
|
||||
# Horizon roadmap
|
||||
|
||||
<!-- Generated by tools/roadmap.py from roadmap/roadmap.toml: edit that file, then run `python3 tools/roadmap.py build`. -->
|
||||
|
||||
What is being done on Horizon, what comes next and where you can help. There are no dates: things are Now, Next or Later. Each item is said in plain words, with the technical picture folded under it. The same roadmap, with a Simple / Technical switch, the board, the journal and how to help, is on [playhorizon.dev/hub](https://playhorizon.dev/hub). Pull requests carry the `roadmap:<id>` label of their item.
|
||||
|
||||
## At a glance
|
||||
|
||||
Horizon is Minecraft: Java Edition 26.3 rewritten from scratch in Rust, so it runs faster on the same computer. Single player, LAN, servers, resource packs and Horizon's own mods work today, on your own copy of the game.
|
||||
|
||||
**Being done now**
|
||||
|
||||
- **Steady frames at long render distances**: Making Horizon faster than vanilla on every computer, with smooth camera movement even at 32 chunks, by spreading the work better over the processor's cores.
|
||||
- **Modpacks' world generation, natively**: Making the world-generation mods of popular modpacks (new structures, YUNG's dungeons and mineshafts, packs built on Lithostitched) generate their worlds in Horizon, natively and fast.
|
||||
- **Playing on servers: everything shows as it should**: When you play on a server, other players, mobs, items and menus look and behave as in vanilla: skins, poses, item names, inventories and server networks.
|
||||
- **Single player as a server: the last differences**: Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
|
||||
- **Horizon's mod registry**: One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
|
||||
|
||||
**Next**: Microsoft sign-in (coming soon); CurseForge in the mod browser (coming soon); Fresh benchmarks against vanilla; Pistons push players and mobs; Horizon on Intel Macs; Faster world creation and entry on small CPUs; Servers that hold 20 TPS with 50 to 100 players; Low-end computers: Steam Deck, old Macs, 8 GB; Native upscaling; A batch of targeted optimisations; A Convert to Rust button for what isn't rewritten yet; Mods with code: machines, magic, mobs; Data packs in the mod browser; Prepare a version without Java; Server console, bans and a full admin API; Hosting polish: Windows service, safe updates; Guided hosting with Tailscale and an invite button; An official Docker image for servers; One single-player path: remove the in-frame world; Villages and combat, exactly as in vanilla; Choose the audio output device; Mods that read and change the world; Machines and blocks with inventories (API 0.4); Lua and other languages for mods; Well-known shader looks, rewritten for Horizon; A visible beta label; Wayland and Hyprland polish; A versioned agent-control protocol; Port mods with your own AI agent; Horizon on X (coming soon); Pipeline protection and a GitHub App for agents; Signed updates everywhere, installers included; A server you install with one command.
|
||||
|
||||
**How to help**: Play it and tell us what differs from vanilla (Discord or GitHub), run the benchmark on your computer, vote for what you want next, or pick one of the Help wanted items.
|
||||
|
||||
**Developers**: Read CONTRIBUTING.md, pick a Help wanted item or a To do card, check vanilla's code for the behaviour, and paste the block `python3 tools/pr_ready.py` prints in your pull request.
|
||||
|
||||
## Now
|
||||
|
||||
### Performance
|
||||
|
||||
#### Steady frames at long render distances
|
||||
|
||||
Making Horizon faster than vanilla on every computer, with smooth camera movement even at 32 chunks, by spreading the work better over the processor's cores.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Frame pacing at render distance 32 ([#678](https://github.com/ludovic111/horizon/issues/678)): long frames come from work competing with the render thread. In review: world generation workers build free structure starts instead of queueing them ([#1760](https://github.com/ludovic111/horizon/issues/1760)), the worker pool leaves one core as vanilla does ([#1778](https://github.com/ludovic111/horizon/issues/1778)), and the terrain vertex arena is read as storage where flat draws run ([#1742](https://github.com/ludovic111/horizon/issues/1742)). Still open: far terrain at 48 to 64 chunks ([#718](https://github.com/ludovic111/horizon/issues/718)) and CPU-bound fields of block entities ([#717](https://github.com/ludovic111/horizon/issues/717)). Every change shows before and after numbers from `tools/perf_bench.py` against vanilla 26.3 ([docs/benchmark.md](docs/benchmark.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Faster block lookups
|
||||
|
||||
Speeding up the code that reads and writes the blocks of the world, which almost everything in the game goes through: generation, lighting, meshing and ticking.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`PalettedContainer` and `PackedStorage` find an entry's word by multiply and shift instead of an integer division ([#1790](https://github.com/ludovic111/horizon/issues/1790), [#1794](https://github.com/ludovic111/horizon/issues/1794)); meshing looks up baked models by index, not by name ([#1747](https://github.com/ludovic111/horizon/issues/1747)). Each change keeps the paletted storage harnesses identical and shows its microbenchmark ([docs/performance/paletted-container-access-results.md](docs/performance/paletted-container-access-results.md)).
|
||||
|
||||
</details>
|
||||
|
||||
### Modpacks
|
||||
|
||||
#### Modpacks' world generation, natively
|
||||
|
||||
Making the world-generation mods of popular modpacks (new structures, YUNG's dungeons and mineshafts, packs built on Lithostitched) generate their worlds in Horizon, natively and fast.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`horizon-port-pack` carries a pack's data-driven content over; structure mods that need code get a native port: YUNG's worldgen as written ([#1746](https://github.com/ludovic111/horizon/issues/1746)), Lithostitched's limited and guaranteed elements with pool and processor modifiers ([#1777](https://github.com/ludovic111/horizon/issues/1777)), its processors as the game's own types ([#1765](https://github.com/ludovic111/horizon/issues/1765)). Proved by the structure blocks and layout harnesses ([#1753](https://github.com/ludovic111/horizon/issues/1753), [#1757](https://github.com/ludovic111/horizon/issues/1757)). Mods whose Java code does more than world generation don't run ([docs/modpacks.md](docs/modpacks.md)).
|
||||
|
||||
</details>
|
||||
|
||||
### Servers and multiplayer
|
||||
|
||||
#### Playing on servers: everything shows as it should
|
||||
|
||||
When you play on a server, other players, mobs, items and menus look and behave as in vanilla: skins, poses, item names, inventories and server networks.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Client-of-a-server fixes in `game/remote*.rs` and `horizon-protocol`: mobs' aggressive and item-use flags ([#1802](https://github.com/ludovic111/horizon/issues/1802), [#1798](https://github.com/ludovic111/horizon/issues/1798)), ghost items after a remote menu ([#1795](https://github.com/ludovic111/horizon/issues/1795)), skins ([#1652](https://github.com/ludovic111/horizon/issues/1652)), cookies and transfer packets for proxy networks ([#1650](https://github.com/ludovic111/horizon/issues/1650)), firework boosts ([#1671](https://github.com/ludovic111/horizon/issues/1671)), living-entity attributes and simulation distance ([#1731](https://github.com/ludovic111/horizon/issues/1731)), styled item names ([#1702](https://github.com/ludovic111/horizon/issues/1702)), primed TNT between updates ([#1710](https://github.com/ludovic111/horizon/issues/1710)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Server dialogs and login screens
|
||||
|
||||
Servers that show dialogs, such as the register and login screens of AuthMe, will work in Horizon.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
26.3's dialog packets and model ([#1720](https://github.com/ludovic111/horizon/issues/1720)), the dialog screen with its buttons and actions ([#1734](https://github.com/ludovic111/horizon/issues/1734)), then text and boolean inputs ([#1735](https://github.com/ludovic111/horizon/issues/1735)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Pre-generate an area with /pregen
|
||||
|
||||
A /pregen command that builds an area of the world ahead of time, so players don't wait for new terrain when they explore.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Generates, lights and saves an area before anyone walks there ([#1805](https://github.com/ludovic111/horizon/issues/1805), a contributor's pull request, in review). Useful on servers, and it moves the generation cost away from the first visit; the generation speed-ups are under 'Steady frames at long render distances'.
|
||||
|
||||
</details>
|
||||
|
||||
### Mods and API
|
||||
|
||||
#### Horizon's mod registry
|
||||
|
||||
One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Merged: the registry index (schema 1, `.hpack`, checks before install, [#1634](https://github.com/ludovic111/horizon/issues/1634)), `cargo horizon-mod check` ([#1642](https://github.com/ludovic111/horizon/issues/1642)) and the submission service (`crates/horizon-mod-registry`, [#1659](https://github.com/ludovic111/horizon/issues/1659)). In review: the website's /mods ([#1596](https://github.com/ludovic111/horizon/issues/1596)) and /mods/submit ([#1635](https://github.com/ludovic111/horizon/issues/1635)), and in the launcher the registry as a source ([#1636](https://github.com/ludovic111/horizon/issues/1636)), install, enable and takedowns ([#1637](https://github.com/ludovic111/horizon/issues/1637)), the Mods page ([#1638](https://github.com/ludovic111/horizon/issues/1638)) and modpacks as their own profiles ([#1640](https://github.com/ludovic111/horizon/issues/1640)).
|
||||
|
||||
More: [docs/mods.md](docs/mods.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Mods that run on the server
|
||||
|
||||
Mods that run on the server side of a world, so a dedicated server or a LAN world can have mods that react to players joining, chat and blocks being broken or placed, and that add commands.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
API 0.3, experimental first ([#1833](https://github.com/ludovic111/horizon/issues/1833), in review): the same sandboxed component runs on the server thread and the dedicated server, with world events (block broken or placed, entity spawned or killed, player joined or left), game rules and server commands with typed arguments. The world, item and entity interfaces come after (see 'Mods that read and change the world'). [docs/api/mods.md](docs/api/mods.md), [docs/mods.md](docs/mods.md).
|
||||
|
||||
</details>
|
||||
|
||||
#### Screens, input and drawing for mods
|
||||
|
||||
Mods will be able to open their own screens and settings pages and react to keys and item use, so bigger mods, such as the Orbital Laser, can run on the normal game.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Capability `ui` is in review ([#1864](https://github.com/ludovic111/horizon/issues/1864)): mod-owned screens and a settings page under Options > Mods, built from vanilla's widgets. Still to design: `input` (an item's use, key binds, the crosshair target), `render` (a budgeted draw list on the native shader API) and `world` (changing blocks). Orbital Laser is the acceptance test: it needs all four today through its author's fork (see 'Orbital Laser on the normal game').
|
||||
|
||||
More: [docs/mods.md](docs/mods.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Tools for people who make mods
|
||||
|
||||
Better tools for mod makers: a test runner that needs no game window, templates for each kind of mod, and clear promises about what keeps working from one version to the next.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`cargo horizon-mod test` runs a mod's test scripts against a fake game snapshot ([#1816](https://github.com/ludovic111/horizon/issues/1816)) and, for server mods, scripted server events ([#1837](https://github.com/ludovic111/horizon/issues/1837)); the content template and the guides are merged ([#1859](https://github.com/ludovic111/horizon/issues/1859), [#1810](https://github.com/ludovic111/horizon/issues/1810), [docs/api/stability.md](docs/api/stability.md)). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference entry is removed without a new major version, and the SDK on crates.io once the API is stable.
|
||||
|
||||
More: [https://playhorizon.dev/docs/test-a-mod](https://playhorizon.dev/docs/test-a-mod)
|
||||
|
||||
</details>
|
||||
|
||||
### Gameplay parity
|
||||
|
||||
#### Single player as a server: the last differences
|
||||
|
||||
Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Since [#1514](https://github.com/ludovic111/horizon/issues/1514) single player runs on the server thread (`game/integrated.rs`) and the window joins it as a client; `--in-frame` keeps the old path. The open `[server-thread]` issues list the gaps. In review: a tool that changes a block ends the click ([#1797](https://github.com/ludovic111/horizon/issues/1797)), potions keep their potion over the connection ([#1796](https://github.com/ludovic111/horizon/issues/1796)), mobs' aggression and arm swings ([#1802](https://github.com/ludovic111/horizon/issues/1802)). [docs/server-levels.md](docs/server-levels.md).
|
||||
|
||||
</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/ludovic111/horizon/issues/1806)), on top of the bundled languages the prepared resources carry ([#1594](https://github.com/ludovic111/horizon/issues/1594)). Left: the client's own screens ([#1672](https://github.com/ludovic111/horizon/issues/1672)) and the main menu with zh_cn ([#1774](https://github.com/ludovic111/horizon/issues/1774)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Cooldowns drawn on items
|
||||
|
||||
After you throw an ender pearl, its slot should show the cooldown sweeping away, as in vanilla.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Vanilla draws `ItemCooldowns`' percent as a white overlay over the slot (`GuiGraphicsExtractor.itemDecorations` and `itemCooldown`) in the hotbar and in inventories. Issue [#1763](https://github.com/ludovic111/horizon/issues/1763).
|
||||
|
||||
More: [#1933](https://github.com/ludovic111/horizon/issues/1933)
|
||||
|
||||
</details>
|
||||
|
||||
#### Chat settings and team prefixes
|
||||
|
||||
The Hidden and Commands Only chat settings should hide chat, and team prefixes should appear before names in chat.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Vanilla checks `ChatVisiblity` before it delivers chat and system messages ([#1376](https://github.com/ludovic111/horizon/issues/1376)), and chat uses the team-formatted display name ([#1378](https://github.com/ludovic111/horizon/issues/1378)). Both issues have exact steps to reproduce.
|
||||
|
||||
More: [#1932](https://github.com/ludovic111/horizon/issues/1932) · [#1841](https://github.com/ludovic111/horizon/issues/1841)
|
||||
|
||||
</details>
|
||||
|
||||
#### A pointing hand over buttons
|
||||
|
||||
The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
26.3's `AbstractWidget` asks for a cursor from `CursorTypes` when it is hovered; winit's `CursorIcon::Pointer` gives the same shape. Issue [#1347](https://github.com/ludovic111/horizon/issues/1347).
|
||||
|
||||
More: [#1897](https://github.com/ludovic111/horizon/issues/1897)
|
||||
|
||||
</details>
|
||||
|
||||
#### Ctrl+Backspace and scroll bars in menus
|
||||
|
||||
Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`EditBox` deletes by word with Ctrl ([#1764](https://github.com/ludovic111/horizon/issues/1764)); the Game Rules and Statistics lists' scroll bars can't be dragged ([#1782](https://github.com/ludovic111/horizon/issues/1782), after [#978](https://github.com/ludovic111/horizon/issues/978)).
|
||||
|
||||
More: [#1884](https://github.com/ludovic111/horizon/issues/1884) · [#1931](https://github.com/ludovic111/horizon/issues/1931)
|
||||
|
||||
</details>
|
||||
|
||||
#### Chickens flap while they fall
|
||||
|
||||
Chickens should flap their wings while they fall, not stand still in the air.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Vanilla's `Chicken.aiStep` updates `flap` and `flapSpeed` when the chicken isn't on the ground, and the model's wings follow them. Issue [#1706](https://github.com/ludovic111/horizon/issues/1706).
|
||||
|
||||
More: [#1892](https://github.com/ludovic111/horizon/issues/1892)
|
||||
|
||||
</details>
|
||||
|
||||
### Rendering
|
||||
|
||||
#### Shader packs, written again from scratch
|
||||
|
||||
Shader packs were removed and are being rebuilt from scratch. They come back, off by default, once the best-known packs draw properly.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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/ludovic111/horizon/issues/1584), [#1585](https://github.com/ludovic111/horizon/issues/1585), [#1586](https://github.com/ludovic111/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/ludovic111/horizon/issues/1698) to [#1801](https://github.com/ludovic111/horizon/issues/1801)).
|
||||
|
||||
More: [#1561](https://github.com/ludovic111/horizon/issues/1561) · [#1771](https://github.com/ludovic111/horizon/issues/1771) · [#1786](https://github.com/ludovic111/horizon/issues/1786)
|
||||
|
||||
</details>
|
||||
|
||||
#### Horizon's own shader format and kit
|
||||
|
||||
Horizon's own way to make shader packs, with tools to write and test them. Every pack is checked before it runs, and the registry will list them.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`horizon-shader-api` (API 0.1, experimental): WGSL stages with typed inputs inside the renderer's passes, options as WGSL override constants, and the `horizon-shader new|check|pack` tool with templates and the example pack Golden Hour. Merged: the check every pack passes ([#1820](https://github.com/ludovic111/horizon/issues/1820)) and the tool, templates and guide ([#1822](https://github.com/ludovic111/horizon/issues/1822)). In review: the post stage running in the game with hot reload ([#1861](https://github.com/ludovic111/horizon/issues/1861)), the sky stage ([#1863](https://github.com/ludovic111/horizon/issues/1863)) and registry checks that use the game's own check ([#1855](https://github.com/ludovic111/horizon/issues/1855)). Left: the material, lighting and shadow stages, a last-good fallback on reload, a GPU-time overlay per stage and budgets ([docs/shaders.md](docs/shaders.md), [docs/api/shaders.md](docs/api/shaders.md)). The Iris/OptiFine loader is a separate item, 'Shader packs, written again from scratch'.
|
||||
|
||||
</details>
|
||||
|
||||
### Horizon extras
|
||||
|
||||
#### Discord Rich Presence
|
||||
|
||||
Your Discord profile can show that you are playing Horizon, with a switch to turn it off.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A Discord IPC client on its own thread ([#1787](https://github.com/ludovic111/horizon/issues/1787)) and the presence with its setting ([#1788](https://github.com/ludovic111/horizon/issues/1788)). Merged: it ships with the next release.
|
||||
|
||||
</details>
|
||||
|
||||
### Code and tools
|
||||
|
||||
#### A leaner codebase that builds faster
|
||||
|
||||
Cleaning up Horizon's code: removing what nothing uses and making it quicker to build, so fixes and new features land faster.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A review of the whole tree: dead code and stubs removed ([#1432](https://github.com/ludovic111/horizon/issues/1432), [#1658](https://github.com/ludovic111/horizon/issues/1658)), one probe binary per crate ([#1453](https://github.com/ludovic111/horizon/issues/1453), [#1454](https://github.com/ludovic111/horizon/issues/1454)), shared harness helpers ([#1448](https://github.com/ludovic111/horizon/issues/1448)), clippy clean on the whole tree ([#1707](https://github.com/ludovic111/horizon/issues/1707)). [docs/architecture.md](docs/architecture.md) maps each feature to its files and harnesses.
|
||||
|
||||
</details>
|
||||
|
||||
## Next
|
||||
|
||||
### Performance
|
||||
|
||||
#### Fresh benchmarks against vanilla
|
||||
|
||||
New public numbers comparing Horizon with vanilla on many kinds of computers, measured the same way every time, with a history that shows what each release changed.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Redo the comparison after the current performance work ([#1073](https://github.com/ludovic111/horizon/issues/1073)); reliability, coverage and presentation ([#1346](https://github.com/ludovic111/horizon/issues/1346)); a scene must count as loaded only once its first chunk arrives ([#1749](https://github.com/ludovic111/horizon/issues/1749)). Next: a performance history across releases (an automatic comparison of each release with the last, on a timeline), more report fields (server TPS and MSPT, power, battery drain, temperature, throttling) and more hardware (a MacBook on battery, low-end laptops, community reports). Players' reports come in through `--benchmark` and `horizon-launcher data benchmark` ([docs/benchmark.md](docs/benchmark.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Faster world creation and entry on small CPUs
|
||||
|
||||
Creating a world and joining it should be quick even on a four-thread laptop, where a fresh world at 32 chunks can make you wait minutes for the first terrain.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Cut the per-column generation cost (noise sampling, surface rules, aquifers) and the lock convoy on structure starts, and leave one core free for the rest of the game as vanilla does ([#1760](https://github.com/ludovic111/horizon/issues/1760) and [#1778](https://github.com/ludovic111/horizon/issues/1778), in review, are listed under 'Steady frames at long render distances'). Still to profile on small CPUs: the first seconds of a fresh world deliver no chunk, and fluids remesh for about a minute after loading. Measured with `tools/perf_bench.py` ([docs/benchmark.md](docs/benchmark.md), [docs/profiling.md](docs/profiling.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Servers that hold 20 TPS with 50 to 100 players
|
||||
|
||||
Servers that keep 20 ticks per second with 50 to 100 players. Today Horizon holds it up to 50 bots and drops to about 10 at 100.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Measured against vanilla on equal work ([docs/server.md](docs/server.md), Scale): Horizon is ahead with few players and when exploring, but vanilla's tick is shorter with 25 to 50 players near spawn. Ideas on the list: send entity changes instead of rebuilding every entity each tick (about 15 percent), fewer memory reads in random ticks and collision scans, cheaper item collision checks, mob AI and natural spawning. The bot benchmark (`horizon-bots`) is the proof.
|
||||
|
||||
</details>
|
||||
|
||||
#### Low-end computers: Steam Deck, old Macs, 8 GB
|
||||
|
||||
Horizon should run well on small machines: integrated graphics, the Steam Deck, older Macs and 8 GB of memory, with a battery mode that saves power on laptops.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far Detail Reduced for integrated GPUs and animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported on the Benchmarks page.
|
||||
|
||||
More: [docs/benchmark.md](docs/benchmark.md) · [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/performance)
|
||||
|
||||
</details>
|
||||
|
||||
#### Native upscaling
|
||||
|
||||
Draw the game at a lower resolution and sharpen it to your screen's size, in the way of FSR or DLSS, to gain frames on weaker graphics cards. An optional graphics setting.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A render-scale setting below the window size with an upscaler of our own that runs on every wgpu backend. Proof: frame time against native resolution on integrated GPUs, and screenshots for image quality. Issue [#1403](https://github.com/ludovic111/horizon/issues/1403).
|
||||
|
||||
</details>
|
||||
|
||||
#### A batch of targeted optimisations
|
||||
|
||||
Several smaller speed-ups, each measured: idle block entities that sleep, cheaper particles, faster structure placement, high-rate mice and very large texture packs.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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/ludovic111/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
|
||||
|
||||
More: [docs/profiling.md](docs/profiling.md)
|
||||
|
||||
</details>
|
||||
|
||||
### Modpacks
|
||||
|
||||
#### CurseForge in the mod browser (coming soon)
|
||||
|
||||
Coming soon: search and download CurseForge mods and modpacks from the launcher, as you can with Modrinth today.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
The mod browser lists CurseForge as coming soon ([docs/mod-browser.md](docs/mod-browser.md)): it needs CurseForge API access. Meanwhile packs import from a CurseForge zip or instance folder, and jars only CurseForge hosts come from the player's own instance ([docs/modpacks.md](docs/modpacks.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### A Convert to Rust button for what isn't rewritten yet
|
||||
|
||||
When a mod, shader pack or modpack hasn't been rewritten for Horizon, one button converts what can be converted, so you don't have to wait for the rewrite.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Generalise the mod browser's Port step (`horizon-port-pack`: the data, assets, worldgen and block and item definitions of Java mods, .mrpack and CurseForge zips) to every kind of content: code mods where the API allows it (see 'Mods with code') and shader packs, converting an Iris/OptiFine pack into Horizon's format. The browser says what works before the install. Java code itself does not run ([docs/modpacks.md](docs/modpacks.md), [docs/mod-browser.md](docs/mod-browser.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Mods with code: machines, magic, mobs
|
||||
|
||||
Making mods that add machines, magic and mobs work in Horizon, in steps, starting with machines. They are most of what a big modpack is made of.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
333 of the 496 mods in a big pack (ATM10) contain code. Issue [#1854](https://github.com/ludovic111/horizon/issues/1854) ranks the API gaps (ticking block entities, networking, rendering, menus, inventories) that unlock them in tiers, with machines first as the recommended path and a furnace ported tick for tick as proof. The first API step is a draft ([#1189](https://github.com/ludovic111/horizon/issues/1189), see 'Machines and blocks with inventories'). The path isn't started yet ([docs/modpacks.md](docs/modpacks.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Data packs in the mod browser
|
||||
|
||||
Search and install data packs from the launcher's mod browser, for the world you choose, as you do mods and resource packs.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A Datapacks tab beside Mods and Resource packs: install into a chosen world's datapacks folder, with the same checks and 'works in Horizon' notes as the other kinds ([docs/mod-browser.md](docs/mod-browser.md)).
|
||||
|
||||
</details>
|
||||
|
||||
### Servers and multiplayer
|
||||
|
||||
#### Prepare a version without Java
|
||||
|
||||
Set up Horizon, a server first, without Java on your computer: published tables are combined with your own copy of the game.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Today `horizon-prepare` runs Mojang's exporters with Java. Issue [#1750](https://github.com/ludovic111/horizon/issues/1750) proposes publishing the computed version tables once from CI and assembling the rest locally from the player's own jar and assets, provided the tables hold no Mojang content; a contributor offered a pull request, and it would help phones too. A server-only prepare from `server.jar` alone is the first step ([docs/server.md](docs/server.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Server console, bans and a full admin API
|
||||
|
||||
Manage a server from a console, ban and pardon players, and drive it from other tools through a full admin API and an MCP server for AI agents.
|
||||
|
||||
<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 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>
|
||||
|
||||
#### Hosting polish: Windows service, safe updates
|
||||
|
||||
Smoother hosting: a real Windows service, a backup before every update, backups that keep the world and each player's state, and a changed view distance or message without a restart.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A Windows service wrapper instead of the logon scheduled task; `backups.before_update`; hot reload of config for view distance and the message of the day (needs engine support); backups that keep world and player state ([#1808](https://github.com/ludovic111/horizon/issues/1808), a contributor's draft, in review). [docs/hosting.md](docs/hosting.md), docs/server.md.
|
||||
|
||||
</details>
|
||||
|
||||
#### Guided hosting with Tailscale and an invite button
|
||||
|
||||
In the launcher, a guide and a button to host a server and invite friends over Tailscale, so nobody has to open ports on a router.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
On the Servers page (create, start, back up, update and invite are shipped) add a Tailscale setup that explains and checks each step, and an invite button that hands friends what they need to join over the tailnet. Buttons first, no docs needed, with the details in an Advanced section. Remote access over Tailscale is already described in [docs/server.md](docs/server.md).
|
||||
|
||||
More: [docs/hosting.md](docs/hosting.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### An official Docker image for servers
|
||||
|
||||
An official image to run a Horizon dedicated server in Docker with one command, your own copy of the game files mounted in.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
The repository has the Dockerfile (`packaging/server/Dockerfile`: a multi-stage build on Debian bookworm-slim, user `horizon`, volume `/data`, port 25565, game files mounted read-only, `HORIZON_EULA` and friends answering the first-start questions), and anyone can build it ([docs/hosting.md](docs/hosting.md), section 9). Left: publish the image to a public registry from the release workflow, tagged per release, so it follows the same checks, and update the docs and the launcher. The image never holds Mojang's files.
|
||||
|
||||
More: [docs/server.md](docs/server.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### A server you install with one command
|
||||
|
||||
Install and run a Horizon dedicated server from a terminal with a single command, no launcher needed, for VPSes and headless machines.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A player's idea (#horizon-ideas). `horizon-server` already runs headless; left: an install script (Linux/macOS shell, Windows PowerShell) that fetches the signed release, checks it, asks for or points at the player's own game files and starts the server, documented in [docs/hosting.md](docs/hosting.md).
|
||||
|
||||
</details>
|
||||
|
||||
### Mods and API
|
||||
|
||||
#### Mods that read and change the world
|
||||
|
||||
Mods that can see and change blocks, read inventories and items, and find and affect entities, all through the server so every player sees the same thing.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
After server mods: read blocks, states and biomes in loaded chunks and change them through the server's block log, so clients and the parity harnesses see vanilla's updates; the player's inventory, held item, open container and item stacks with components; entity queries near a position, a safe subset of entity data and data-driven mod entity types. Snapshots and batched calls per tick, never per-block host calls in hot paths. [docs/api/mods.md](docs/api/mods.md) ('Not there yet').
|
||||
|
||||
More: [docs/modpacks.md](docs/modpacks.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Machines and blocks with inventories (API 0.4)
|
||||
|
||||
Mods that add machines: blocks that tick, hold items and have their own menus. This unlocks the technology mods of big modpacks.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Block entities and menus: ticking machines with slots, server menu logic and the menu protocol to clients, energy, item and fluid transfer, data attachments, accessory slots and custom recipe types, with a typed client and server channel and typed config beside them. A draft ([#1189](https://github.com/ludovic111/horizon/issues/1189)) proves a furnace ported tick for tick and holds the ATM10 code-mods plan; the design is redone through the API policy before it merges ([docs/modpacks.md](docs/modpacks.md), docs/api/stability.md).
|
||||
|
||||
</details>
|
||||
|
||||
#### Lua and other languages for mods
|
||||
|
||||
Write mods as Lua scripts, in the style of KubeJS, with no compiler, and later in other languages that pass the tests. Every language runs in the same sandbox.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Lua first: Horizon ships a Lua interpreter that is itself a WebAssembly mod, and a script mod's manifest names it and the capabilities it uses, so a script can do exactly what a compiled mod with the same manifest can, with hot reload of `main.lua`. Then C, C++, Zig, Go (TinyGo), AssemblyScript and JavaScript or TypeScript are measured against Rust on the same tick-heavy example, and each is supported only with a template, an example and a CI test. [docs/api/mods.md](docs/api/mods.md) ('Ways in').
|
||||
|
||||
</details>
|
||||
|
||||
### Gameplay parity
|
||||
|
||||
#### Pistons push players and mobs
|
||||
|
||||
Pistons and slime blocks will push players, mobs and items as in vanilla. Today moving blocks pass through them.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Port `PistonMovingBlockEntity.moveCollidedEntities` and `moveStuckEntities` (honey), the slime bounce and `Entity.limitPistonMovement` for `MoverType.PISTON`, in single player and for a server's players. Issues [#1496](https://github.com/ludovic111/horizon/issues/1496), [#1609](https://github.com/ludovic111/horizon/issues/1609), [#1608](https://github.com/ludovic111/horizon/issues/1608), [#1704](https://github.com/ludovic111/horizon/issues/1704), [#1758](https://github.com/ludovic111/horizon/issues/1758).
|
||||
|
||||
</details>
|
||||
|
||||
#### One single-player path: remove the in-frame world
|
||||
|
||||
Single player will have one way to run, the server thread, so every fix works everywhere and there are fewer bugs. The older in-frame mode goes once everything passes without it.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`--in-frame` (also `--no-server-thread` and `HORIZON_SERVER_THREAD=0`) still ticks the world inside the window's `Game`, and most harnesses test that path. Delete it once `lan_e2e --server-thread` and the parity harnesses pass on the server thread, which also halves the bug surface. See also 'Single player as a server' and [docs/server-levels.md](docs/server-levels.md).
|
||||
|
||||
</details>
|
||||
|
||||
#### Villages and combat, exactly as in vanilla
|
||||
|
||||
Villages and fights behaving as in vanilla: iron golems defending villages, villagers remembering their homes, mace smashes and knockback.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Villages: iron golem village goals, villager memories across saves, villager age, job-site claiming on dedicated servers, flower offers ([#1639](https://github.com/ludovic111/horizon/issues/1639) is one report). Combat: the mobs' `attack_knockback` attribute, mace smash logic, the hit knockback source on the host, the fire burnout biome tag. Each is checked against vanilla by a harness ([docs/rust-parity.md](docs/rust-parity.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Choose the audio output device
|
||||
|
||||
Pick the speakers or headphones Horizon plays through, in Music & Sounds.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
An output device list in Music & Sounds, falling back to the system default. [#1035](https://github.com/ludovic111/horizon/issues/1035) started it: a player on an older Windows computer had no sound at all, and a device choice is how such cases are found and fixed.
|
||||
|
||||
</details>
|
||||
|
||||
### Rendering
|
||||
|
||||
#### Well-known shader looks, rewritten for Horizon
|
||||
|
||||
Popular shader packs rewritten in Horizon's format, so good packs exist from the start. Only packs whose licence is open source and compatible with ours are rewritten, never anything else.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
The first candidates are Photon, Chocapic13 and MakeUp Ultra Fast, each only after its licence is read and recorded: a pack is rewritten only if it is open source and compatible with Horizon's licence, and the rewrite is written from the shader format's documentation and the look on screen, not from anyone's code. Today only the Golden Hour example exists (`[crates/horizon-shader-api/packs/golden_hour](crates/horizon-shader-api/packs/golden_hour)`).
|
||||
|
||||
More: [docs/shaders.md](docs/shaders.md)
|
||||
|
||||
</details>
|
||||
|
||||
### Launcher
|
||||
|
||||
#### Microsoft sign-in (coming soon)
|
||||
|
||||
Coming soon: sign in with your Microsoft account to play on online-mode servers. The code is ready; it waits for Microsoft's approval.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Device-code sign-in in the launcher and online-mode login in the client ([docs/microsoft-account.md](docs/microsoft-account.md)). It is turned on by setting the app's client id once Minecraft API access is approved; until then the launcher shows "Coming soon".
|
||||
|
||||
More: [#1270](https://github.com/ludovic111/horizon/issues/1270)
|
||||
|
||||
</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.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Show Beta in the launcher's header and about page, on the game's title screen and on the website, beside the version number ([docs/versioning.md](docs/versioning.md)).
|
||||
|
||||
</details>
|
||||
|
||||
### Platforms
|
||||
|
||||
#### Horizon on Intel Macs
|
||||
|
||||
Builds of the game and the launcher for Macs with an Intel processor.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
x86_64 game and launcher builds, picked per processor ([#1171](https://github.com/ludovic111/horizon/issues/1171)). Before it ships it needs a run on a real Intel Mac: Rosetta on Apple Silicon only proves the CPU code. Requests: [#1161](https://github.com/ludovic111/horizon/issues/1161), [#1792](https://github.com/ludovic111/horizon/issues/1792).
|
||||
|
||||
</details>
|
||||
|
||||
#### Wayland and Hyprland polish
|
||||
|
||||
Fixes for Linux desktops that use Wayland and Hyprland: fullscreen, the clipboard, the GUI scale after a restart and the like.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Open: the GUI Scale is clamped to 2 after a restart under Hyprland, while XWayland keeps it ([#1866](https://github.com/ludovic111/horizon/issues/1866)). Earlier reports covered fullscreen dropping on menu clicks, the clipboard and a white overlay ([#1007](https://github.com/ludovic111/horizon/issues/1007), closed). Checked on a real Wayland session.
|
||||
|
||||
</details>
|
||||
|
||||
### AI and agents
|
||||
|
||||
#### A versioned agent-control protocol
|
||||
|
||||
A stable, documented way for AI agents and scripts to drive the game and the launcher, with versions and an event stream so tools keep working as Horizon changes.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`hello` answers with the API version, the release and the features; `horizon-ctl --api`; MCP serverInfo; reply schemas generated into the reference; a `test.` namespace for hooks; transcript compatibility tests; `subscribe` as a stream of events on one connection; Horizon option files by key and saved-server commands. Agent control itself (`horizon-ctl`, both MCP servers, the No AI switch) is done: [docs/agent-control.md](docs/agent-control.md), [docs/api/agent-control.md](docs/api/agent-control.md).
|
||||
|
||||
More: [docs/api/stability.md](docs/api/stability.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Port mods with your own AI agent
|
||||
|
||||
You can port a mod to Horizon with your own AI agent on your own computer. Horizon documents how and gives you the tools, but provides no hosted agents.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide (playhorizon.dev/docs/agents, [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
|
||||
|
||||
More: [https://playhorizon.dev/docs/agents](https://playhorizon.dev/docs/agents)
|
||||
|
||||
</details>
|
||||
|
||||
### Community
|
||||
|
||||
#### Horizon on X (coming soon)
|
||||
|
||||
Coming soon: an official Horizon account on X, used to announce releases and the weekly summary.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Kira already posts the roadmap items marked `announce = true` when they ship, and the weekly summary, as soon as the account's credentials exist ([roadmap/README.md](roadmap/README.md), 'The journal'). Waiting for the account itself.
|
||||
|
||||
</details>
|
||||
|
||||
### Code and tools
|
||||
|
||||
#### Pipeline protection and a GitHub App for agents
|
||||
|
||||
Safer releases: changes to the release machinery need the owner's review, and AI agents use a smaller identity of their own instead of a personal account.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`.github/CODEOWNERS` lists the workflows, the release and signing tools, the update-checking code and the installers ([#1015](https://github.com/ludovic111/horizon/issues/1015)); the branch protection that makes it count waits for the `horizon-agent` GitHub App, which reads and writes code, pull requests and issues but can't change settings, workflows, secrets or members, and whose tokens last an hour. It needs the owner to create the App and switch the protection on ([docs/security.md](docs/security.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Signed updates everywhere, installers included
|
||||
|
||||
Every download Horizon makes is checked against the project's signature, including the first install with install.sh and install.ps1.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/ludovic111/horizon/issues/1011), [#1014](https://github.com/ludovic111/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)).
|
||||
|
||||
</details>
|
||||
|
||||
## Later
|
||||
|
||||
### Performance
|
||||
|
||||
#### Exact far terrain from real chunks
|
||||
|
||||
Far terrain drawn from the world you really built and explored, caves and builds included, not only a preview from the seed.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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/ludovic111/horizon/issues/718).
|
||||
|
||||
</details>
|
||||
|
||||
### Modpacks
|
||||
|
||||
#### Older Fabric and Forge mods
|
||||
|
||||
Recognise mods built for versions before 26.1 in the mod browser, so they can be checked and carried over like newer ones.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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)).
|
||||
|
||||
</details>
|
||||
|
||||
#### A modpack's own resource and shader packs
|
||||
|
||||
Importing a modpack also brings the resource packs and shader packs it ships with, where Horizon can use them.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Porting a pack carries data, assets and worldgen today; the resource packs and shader packs bundled with it are left out. Resource packs can be carried as they are; shader packs go through the converter ([docs/modpacks.md](docs/modpacks.md)).
|
||||
|
||||
</details>
|
||||
|
||||
#### Whole modpacks, such as ATM10, rewritten
|
||||
|
||||
Rewriting a complete big modpack for Horizon so it runs far better than the original. Not a priority now: the work above comes first.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
The project's first goal for modpacks: support them ten times better than vanilla and port any of them. It was set aside in favour of compatibility through the browser. What exists: ATM10's world generates and 42 of its 496 mods are carried at least in part ([docs/modpacks.md](docs/modpacks.md)). It depends on 'Mods with code' and the native mod API.
|
||||
|
||||
</details>
|
||||
|
||||
#### Running Java mods that aren't rewritten
|
||||
|
||||
Run Java mods that nobody has rewritten for Horizon yet, through a bridge or a converter. Hard and not started: the Convert button and native rewrites come first.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Earlier research ([#527](https://github.com/ludovic111/horizon/issues/527), closed) found that Java mods can't simply run, because they call Minecraft's own classes. Options to look at again: a jar-to-WebAssembly backend, or a bridge for simple Fabric mods. Whatever is chosen has to run inside the same sandbox and capability checks as native mods.
|
||||
|
||||
</details>
|
||||
|
||||
### Servers and multiplayer
|
||||
|
||||
#### Ely.by and other sign-in providers
|
||||
|
||||
Sign in with other account providers, such as Ely.by or any Yggdrasil-compatible one, beside Microsoft.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#1270](https://github.com/ludovic111/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>
|
||||
|
||||
#### Online-mode checks for Play with Friends
|
||||
|
||||
Friends who join through an invite code could prove who they are with their Microsoft account, as on online-mode servers.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Play with Friends joins over an encrypted tunnel with an invite code ([docs/play-with-friends.md](docs/play-with-friends.md), 'Identity'). Online-mode checks against Mojang's session servers can be added on top of the same tunnel; they need Microsoft sign-in first, which is coming soon. Play with Friends stays self-hosted between players, with no relay run by the project.
|
||||
|
||||
</details>
|
||||
|
||||
### Mods and API
|
||||
|
||||
#### Orbital Laser on the normal game
|
||||
|
||||
The community mod Orbital Laser, made for a fork of Horizon, running on the normal game and listed in the registry as the showcase of the mod API.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A pull request on [https://github.com/Stink-O/orbital-laser](https://github.com/Stink-O/orbital-laser) (MIT) once the input, ui, render and world capabilities exist (see 'Screens, input and drawing for mods'): today the mod needs them through its author's fork. Tested in the game, and listed in the registry.
|
||||
|
||||
</details>
|
||||
|
||||
#### Native versions of popular client mods
|
||||
|
||||
Built-in versions of the client mods players ask for: block info on hover, a recipe viewer like JEI, a world map and schematic overlays.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Done as extras: a mod list and menu, a minimap and a zoom ([docs/extras.md](docs/extras.md)). Left: block info tooltips, a recipe viewer (a contributor offered to port JEI if it becomes a native feature), a world map and a schematic overlay. They need the read-only interfaces `inventory-read`, `recipes`, `world-read` and `render` in the mod API, or they go into the game directly.
|
||||
|
||||
More: [docs/mods.md](docs/mods.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Create-style physics as a native mod
|
||||
|
||||
Contraptions and physics like the Create and Aeronautics mods, as a mod for Horizon rather than part of the base game.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#805](https://github.com/ludovic111/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod on the mod API. It needs server mods, entities and rendering first.
|
||||
|
||||
</details>
|
||||
|
||||
#### Proximity voice chat as a native mod
|
||||
|
||||
Hear nearby players talk, as with the voice chat mods of other Minecraft versions, as a Horizon mod.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Needs server mods, mod networking and an audio interface in the mod API before anything can be built ([docs/api/mods.md](docs/api/mods.md)).
|
||||
|
||||
</details>
|
||||
|
||||
### Gameplay parity
|
||||
|
||||
#### Every Minecraft version, prepared automatically
|
||||
|
||||
Every past and future Minecraft version prepared for Horizon automatically. When Mojang releases one, Horizon follows it, and every system is checked against it again.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
#### Controllers: tested on real pads, and in the launcher
|
||||
|
||||
Make controller support solid: tested with real PlayStation and Xbox pads and a Steam Deck, and usable in the launcher too.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
#### The full debug screen
|
||||
|
||||
Vanilla's complete F3 screen: debug options, a profiler chart, ten-second captures and an atlas dump.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Today F3 shows position, chunk, light, biome and FPS ([docs/native-client.md](docs/native-client.md)). Vanilla also has F3+F6 debug options with profiles, F3+1 the profiler pie, F3+L a ten-second capture and F3+S the atlas dump ([#562](https://github.com/ludovic111/horizon/issues/562) asked for the F3 editor).
|
||||
|
||||
</details>
|
||||
|
||||
### Rendering
|
||||
|
||||
#### A light default shader pack
|
||||
|
||||
A small built-in look (tone mapping, colour grade, a cheap vignette) that costs almost nothing, for players who want a nicer picture without a heavy pack.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
An opt-in, composite-only pack on the native shader API, under about 0.3 ms at 1080p, shown on the Shader Packs page. Pack option sliders and profiles, and a Failed row with the full error, belong with it.
|
||||
|
||||
More: [docs/shaders.md](docs/shaders.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Optional ray tracing
|
||||
|
||||
An optional ray-tracing mode for graphics cards that support it (RTX and recent AMD): shadows, water reflections and global lighting on top of the vanilla look.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A spike with ray-traced sun shadows, off by default, is a draft ([#1180](https://github.com/ludovic111/horizon/issues/1180)). Reflections, global illumination, Apple M3 and newer if it is cheap, and a performance report per card come after.
|
||||
|
||||
</details>
|
||||
|
||||
#### HDR monitors
|
||||
|
||||
Output for HDR monitors: brighter highlights and richer colour where your screen supports it.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#1441](https://github.com/ludovic111/horizon/issues/1441). Needs a surface format and a tone-mapping path per backend, and tests on real HDR screens on Windows, macOS and Linux.
|
||||
|
||||
</details>
|
||||
|
||||
#### MSAA, anisotropic filtering, exclusive fullscreen
|
||||
|
||||
More graphics options: multisample anti-aliasing, anisotropic filtering and an exclusive fullscreen mode. Today Horizon has FXAA and borderless fullscreen.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Not implemented ([docs/graphics.md](docs/graphics.md)). Anisotropic filtering needs linear magnification in wgpu, which blurs close blocks, so close-up sharpness has to be kept first; MSAA would sit beside FXAA; exclusive fullscreen where the backend supports it.
|
||||
|
||||
</details>
|
||||
|
||||
### Launcher
|
||||
|
||||
#### Lean installs that use less disk
|
||||
|
||||
Smaller installs: Horizon keeps one shared copy of the prepared game files and removes what only the preparation needed.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A shared resource store keyed by the preparer's digest, no prepare intermediates left behind, the current and the previous version kept, and an automatic clean-up. Not started.
|
||||
|
||||
</details>
|
||||
|
||||
### Platforms
|
||||
|
||||
#### Horizon on Linux ARM64
|
||||
|
||||
Game and launcher builds for 64-bit ARM Linux computers.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#1709](https://github.com/ludovic111/horizon/issues/1709): aarch64 builds of the game and the launcher in the release workflow, tested on a real ARM machine with its Vulkan driver. The server build already drops audio and controllers (`--no-default-features`).
|
||||
|
||||
</details>
|
||||
|
||||
#### Android, iOS and the browser
|
||||
|
||||
Versions of Horizon for Android, iOS and web browsers. Nothing official yet; contributors have started the first pieces.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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 WebAssembly build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/ludovic111/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/ludovic111/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/ludovic111/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
|
||||
|
||||
</details>
|
||||
|
||||
#### AppImage and a Nix flake
|
||||
|
||||
Install Horizon on any Linux distribution as an AppImage, and on NixOS through a Nix flake.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A player's idea (#horizon-ideas). An AppImage of the launcher and game from the release workflow, and a flake.nix packaging the release binaries with their runtime libraries (Vulkan loader, X11/Wayland, ALSA), the way horizon-parity's NixOS setup runs them today.
|
||||
|
||||
</details>
|
||||
|
||||
### Horizon extras
|
||||
|
||||
#### A complete video studio inside the game
|
||||
|
||||
Record, replay and edit videos without leaving the game: record in a keypress, replay a session, cut and finish the video. One of Horizon's original goals.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
#### Game menus in the launcher's style
|
||||
|
||||
An optional look for the game's own menus and options that matches the Horizon launcher. Off by default: the vanilla menus stay as they are.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
A player's idea (#horizon-ideas). A built-in resource pack or a Horizon setting that restyles the title, pause and options screens with the launcher's design tokens (crates/horizon-theme), following [docs/design-system.md](docs/design-system.md); off by default because it changes the vanilla look.
|
||||
|
||||
</details>
|
||||
|
||||
### AI and agents
|
||||
|
||||
#### An optional AI teammate
|
||||
|
||||
An optional AI teammate you can talk to by voice. It plays beside you, helps or finishes the game with you, runs on your computer first, and exists only if you turn it on. The No AI switch hides it.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Draft pull requests: the companion ([#1068](https://github.com/ludovic111/horizon/issues/1068)), its safety modes and their evaluation ([#1069](https://github.com/ludovic111/horizon/issues/1069)) and a route that finishes the game ([#1070](https://github.com/ludovic111/horizon/issues/1070)). In the drafts: a companion that follows, fights, mines, crafts, sleeps and respawns; chat and push-to-talk voice, with speech recognition and a choice of voices; presets (very strong, helper, beat the game); a guard model with Clean, Kid and Salty modes; perception levels from Fair to All-knowing; hardware detection with a simple mode and an Advanced tab; local models first, cloud keys optional. The No AI switch is already in the game. Left: the speedrun route doesn't finish the game reliably yet, voices for more languages, and the safety evaluation, which must pass before it ships.
|
||||
|
||||
</details>
|
||||
|
||||
### Community
|
||||
|
||||
#### A video series about Horizon
|
||||
|
||||
A series of videos about how Horizon is made and what is new, with each upload announced on Discord.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Kira already shows new uploads in a read-only Discord channel (done). The series follows each stage of the project: a script, edits, titles and a thumbnail per video.
|
||||
|
||||
</details>
|
||||
|
||||
## Help wanted
|
||||
|
||||
### Performance
|
||||
|
||||
#### Benchmark your computer
|
||||
|
||||
Run Horizon's built-in benchmark and share the report: every new computer makes the performance work more accurate. No coding needed.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it (playhorizon.dev/docs/performance). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
|
||||
|
||||
More: [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/performance)
|
||||
|
||||
</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/ludovic111/horizon/issues/1438); possibly related: [#526](https://github.com/ludovic111/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/ludovic111/horizon/issues/1437), [#1660](https://github.com/ludovic111/horizon/issues/1660).
|
||||
|
||||
</details>
|
||||
|
||||
## Recently shipped
|
||||
|
||||
- **OpenGL for old computers** (v0.2.63): Horizon now starts on old PCs whose graphics card has no Vulkan or DirectX 12 driver: it switches to OpenGL by itself, and Options > Graphics can pick OpenGL by hand. [#1917](https://github.com/ludovic111/horizon/issues/1917) · [#1920](https://github.com/ludovic111/horizon/issues/1920) · [#1936](https://github.com/ludovic111/horizon/issues/1936)
|
||||
- **Server resource packs** (v0.2.48): Servers can send you their resource pack: Horizon asks, downloads it and applies it, like vanilla. [#1562](https://github.com/ludovic111/horizon/issues/1562) · [#1569](https://github.com/ludovic111/horizon/issues/1569) · [#1570](https://github.com/ludovic111/horizon/issues/1570)
|
||||
- **Single player on its own server thread** (v0.2.39): Single player runs on a server thread, as vanilla does, so the world keeps ticking smoothly while the window draws. [#1514](https://github.com/ludovic111/horizon/issues/1514) · [docs/server-levels.md](docs/server-levels.md)
|
||||
- **GPU occlusion culling on Macs** (v0.2.17): On Macs the GPU now skips terrain hidden behind hills and caves: 8 to 48% more frames per second at 32 chunks on an M4 Pro. [#1434](https://github.com/ludovic111/horizon/issues/1434)
|
||||
- **Animated textures on the GPU** (v0.2.5): Water, lava and other animated blocks are now animated by the graphics card: 1.7 to 2.4 times better 1% lows on Intel graphics. [#1239](https://github.com/ludovic111/horizon/issues/1239)
|
||||
- **Far Detail for integrated graphics** (v0.2.5): An optional Reduced setting for far terrain, suggested on integrated graphics, that saves 2 to 4 ms of GPU time per frame. [#1199](https://github.com/ludovic111/horizon/issues/1199) · [#1029](https://github.com/ludovic111/horizon/issues/1029)
|
||||
- **The launcher's mod browser** (launcher@0.2.0): Search Modrinth for mods, modpacks and resource packs from the launcher; it says before installing what works in Horizon. [#1154](https://github.com/ludovic111/horizon/issues/1154) · [#1242](https://github.com/ludovic111/horizon/issues/1242) · [#1243](https://github.com/ludovic111/horizon/issues/1243) · [#1246](https://github.com/ludovic111/horizon/issues/1246) · [docs/mod-browser.md](docs/mod-browser.md)
|
||||
- **A blocky, Minecraft-like launcher** (launcher@0.2.0): The launcher got a new blocky look in Horizon's purple, in dark and light. [#1269](https://github.com/ludovic111/horizon/issues/1269) · [docs/design-system.md](docs/design-system.md)
|
||||
- **Dynamic lights** (v2026.10.22): An optional extra: torches you hold, drop or set on fire light up what is around them. [#1162](https://github.com/ludovic111/horizon/issues/1162)
|
||||
- **Host a server from the launcher** (launcher-v0.15.0): The launcher's Servers page creates, starts, backs up and updates a Horizon server with buttons, and invites friends. [#1026](https://github.com/ludovic111/horizon/issues/1026) · [docs/hosting.md](docs/hosting.md)
|
||||
+24
-14
@@ -1,21 +1,31 @@
|
||||
# Third-party notices
|
||||
|
||||
## wgpu-hal
|
||||
## CullLeaves
|
||||
|
||||
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
|
||||
developers, with one upstream fix applied
|
||||
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124)). It is licensed under MIT or
|
||||
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
The optional leaf and mangrove-root face-culling behavior is adapted from
|
||||
[CullLeaves](https://github.com/TeamMidnightDust/CullLeaves) by MidnightDust.
|
||||
Its MIT license is reproduced below.
|
||||
|
||||
## Pixelify Sans
|
||||
```text
|
||||
MIT License
|
||||
|
||||
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
|
||||
by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1.1
|
||||
(`crates/horizon-theme/fonts/OFL-PixelifySans.txt`).
|
||||
Copyright (c) 2020 MidnightDust
|
||||
|
||||
## Pixelarticons
|
||||
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:
|
||||
|
||||
`crates/horizon-theme/icons/pixel/*.svg` are icons from
|
||||
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
|
||||
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`). `star.svg` is Horizon's own, drawn on
|
||||
the same grid.
|
||||
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.
|
||||
```
|
||||
@@ -1,174 +0,0 @@
|
||||
# Dedicated server against vanilla 26.3, with bots (2026-10-17)
|
||||
|
||||
`horizon-client --dedicated` (run by `horizon-server`) against the pinned vanilla 26.3 `server.jar`, on `main`
|
||||
at `7bbbf6e9` (in release v2026.10.17.17). That build has the movement checks and chunk pacing of #586, the
|
||||
equal-work method of #670, and the server performance PRs #701–#707 and #713.
|
||||
|
||||
This replaces the data first published here, which came from a Mac mini overloaded by other work (load 55–141 on
|
||||
14 threads). An earlier run of this method, on `main` at `002f14ce`, is kept in `earlier-002f14ce/` and compared
|
||||
at the end.
|
||||
|
||||
## Setup
|
||||
|
||||
- **Server machine:** ThinkPad, Intel i7-7500U (2 cores, 4 threads), 32 GB, NixOS. Nothing else heavy ran on it:
|
||||
its load average was 0.4 when idle. Both servers ran under `nice -n 19`.
|
||||
- **Bots:** `horizon-bots` on another machine (Mac mini M4 Pro), over the tailnet. That machine was shared with
|
||||
other work (load 36–170 on 14 threads, mean 78). The bots need little CPU, but a busy bot host can answer later.
|
||||
Both servers saw the same conditions, since they took turns at each bot count.
|
||||
- **Horizon:** release build of `7bbbf6e9` (`horizon-server`, `horizon-client`).
|
||||
- **Vanilla:** the pinned 26.3 `server.jar` on OpenJDK 25.0.4.1, with `-Xms4g -Xmx4g`.
|
||||
- **Properties:**
|
||||
- `level-seed=12345`, view and simulation distance 10, creative (forced);
|
||||
- `online-mode=false`, `difficulty=easy`, `max-players=200`;
|
||||
- default game rules on both (the locator bar is on).
|
||||
- **Each run:** a fresh server, bots joining one per second, a 60 s warm-up, then 180 s measured.
|
||||
- **Commands** (on the server machine):
|
||||
```
|
||||
python3 tools/server_bench.py --servers horizon,vanilla --nice 19 --warmup 60 --seconds 180 \
|
||||
--bots-via "ssh <bot host>" --bots-bin <horizon-bots> --connect <server address> \
|
||||
--bots 1,10,25,50,100 --behaviour walk,explore
|
||||
python3 tools/server_bench.py ...same... --bots 1,10,25,50 --behaviour explore --explore-speed 0.8
|
||||
```
|
||||
- **Columns:**
|
||||
- **MSPT** (mean) and **P95** come from `/tick query`, in ms. **TPS** is the tick rate.
|
||||
- **RSS** is the mean over the run. **CPU** is the whole process tree, as a percentage of one core.
|
||||
- **Chunks/s** is what the bots received. **New chunks/s** counts each chunk once per bot.
|
||||
- **Pulled back** counts the times the server moved a bot back (vanilla's "moved too quickly" and similar).
|
||||
- **Load** is the mean load average during that run: the server machine (which includes the server's own
|
||||
threads) and the bot host.
|
||||
|
||||
## Results
|
||||
|
||||
### Equal work: bots explore at the same pace (`--explore-speed 0.8`)
|
||||
|
||||
Each bot flies outwards at 0.8 blocks a second, which both servers keep up with. They send the same chunks, so
|
||||
the tick times compare the same work.
|
||||
|
||||
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 3.16 | 5.78 | 20.0 | 0.76 GB | 22 % | 2 | 1.27 | 0 | 2.8 / 63 |
|
||||
| 1 | Vanilla | 16.82 | 27.3 | 20.0 | 3.37 GB | 69 % | 2 | 1.35 | 0 | 1.8 / 94 |
|
||||
| 10 | Horizon | 9.83 | 19.01 | 20.0 | 0.86 GB | 77 % | 14 | 12.31 | 1051 | 2.2 / 74 |
|
||||
| 10 | Vanilla | 24.38 | 39.58 | 20.0 | 3.57 GB | 132 % | 14 | 12.32 | 1027 | 2.5 / 64 |
|
||||
| 25 | Horizon | 16.87 | 30.88 | 20.0 | 0.89 GB | 111 % | 34 | 29.92 | 3770 | 5.1 / 51 |
|
||||
| 25 | Vanilla | 30.29 | 43.47 | 20.0 | 3.61 GB | 176 % | 33 | 29.29 | 5139 | 5.2 / 55 |
|
||||
| 50 | Horizon | 23.57 | 47.53 | 20.0 | 0.92 GB | 127 % | 67 | 59.42 | 7434 | 8.5 / 64 |
|
||||
| 50 | Vanilla | 42.44 | 54.84 | 20.0 | 3.67 GB | 201 % | 65 | 58.11 | 10604 | 5.1 / 59 |
|
||||
|
||||
### `walk`: bots walk around the spawn
|
||||
|
||||
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 4.01 | 8.19 | 20.0 | 0.78 GB | 59 % | 10 | 0.43 | 0 | 1.4 / 70 |
|
||||
| 1 | Vanilla | 18.22 | 29.34 | 20.0 | 3.43 GB | 69 % | 10 | 0.47 | 0 | 1.9 / 63 |
|
||||
| 10 | Horizon | 8.79 | 17.35 | 20.0 | 0.78 GB | 119 % | 95 | 3.58 | 0 | 2.9 / 69 |
|
||||
| 10 | Vanilla | 18.67 | 38.26 | 20.0 | 3.50 GB | 101 % | 101 | 4.38 | 5 | 3.2 / 59 |
|
||||
| 25 | Horizon | 15.01 | 31.12 | 20.0 | 0.80 GB | 161 % | 234 | 8.49 | 0 | 6.3 / 43 |
|
||||
| 25 | Vanilla | 18.78 | 28.16 | 20.0 | 3.52 GB | 155 % | 252 | 11.89 | 10 | 6.7 / 57 |
|
||||
| 50 | Horizon | 22.72 | 60.19 | 20.0 | 0.81 GB | 180 % | 465 | 16.94 | 0 | 14.7 / 50 |
|
||||
| 50 | Vanilla | 34.3 | 55.55 | 20.0 | 3.58 GB | 267 % | 497 | 26.74 | 39 | 9.5 / 56 |
|
||||
| 100 | Horizon | 63.33 | 125.04 | 15.79 | 0.87 GB | 246 % | 678 | 31.8 | 0 | 32.9 / 95 |
|
||||
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 15.1 / 120 |
|
||||
|
||||
Both servers sent each walking bot the same number of distinct chunks over the whole run (680–690). Horizon's
|
||||
"new chunks/s" inside the measured window is lower, so compare the totals, not that column.
|
||||
|
||||
### `explore` at full speed: unequal work
|
||||
|
||||
Bots fly outwards as fast as chunks arrive (`horizon-bots`' default, up to 22 blocks a second). Both servers now pull them back ("moved too quickly"), but Horizon sends
|
||||
more new terrain, so this pass doesn't compare equal work. The server-machine load of Horizon's 1-bot run is
|
||||
estimated from the time between the runs around it: its log was overwritten before it was read.
|
||||
|
||||
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 9.73 | 20.99 | 20.0 | 0.84 GB | 330 % | 41 | 35.96 | 0 | 4.7 / 129 |
|
||||
| 1 | Vanilla | 9.71 | 20.37 | 20.0 | 3.61 GB | 247 % | 35 | 33.61 | 0 | 4.3 / 156 |
|
||||
| 10 | Horizon | 21.27 | 39.15 | 20.0 | 1.18 GB | 322 % | 46 | 46.11 | 9856 | 5.0 / 114 |
|
||||
| 10 | Vanilla | 16.94 | 30.28 | 20.0 | 3.95 GB | 254 % | 25 | 25.01 | 11123 | 5.5 / 89 |
|
||||
| 25 | Horizon | 25.76 | 45.01 | 20.0 | 1.17 GB | 314 % | 61 | 60.7 | 31264 | 6.6 / 82 |
|
||||
| 25 | Vanilla | 24.93 | 37.09 | 20.0 | 4.04 GB | 305 % | 38 | 37.84 | 33467 | 6.1 / 91 |
|
||||
| 50 | Horizon | 39.31 | 65.33 | 20.0 | 1.25 GB | 325 % | 115 | 112.25 | 64163 | 6.9 / 95 |
|
||||
| 50 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 7.7 / 86 |
|
||||
| 100 | Horizon | 52.7 | 85.73 | 18.97 | 1.36 GB | 328 % | 154 | 151.83 | 116161 | 13.4 / 79 |
|
||||
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 10.0 / 72 |
|
||||
|
||||
Vanilla's watchdog stopped it after a 60-second tick in three runs (walk 100, explore 50 and 100). Each time its
|
||||
server thread was in entity tracking (`ChunkMap$TrackedEntity.updatePlayer`).
|
||||
|
||||
## What it shows
|
||||
|
||||
- **Same work, 1 to 50 players:** Horizon's mean tick is 44–81 % shorter than vanilla's, its 95th percentile is
|
||||
lower, and its process uses 32–63 % of vanilla's CPU.
|
||||
- **Walking, 1 to 50 players:** Horizon's mean tick is shorter at every count (4.0 against 18.2 ms with 1 bot,
|
||||
22.7 against 34.3 ms with 50).
|
||||
- **Where vanilla wins:**
|
||||
- With 25 and 50 walking bots, vanilla's 95th percentile is lower (28.2 against 31.1 ms, 55.6 against
|
||||
60.2 ms). Horizon's slow ticks are spikier.
|
||||
- In the full-speed explore pass, vanilla's mean tick is shorter with 10 and 25 bots (16.9 against 21.3 ms,
|
||||
24.9 against 25.8 ms), while Horizon sends 1.6–1.8 times as many new chunks.
|
||||
- With 50 walking bots, Horizon makes the server machine busier (load 14.7 against 9.5). Its connection
|
||||
writer threads compress every packet (below).
|
||||
- **100 players:** Horizon keeps 15.8 TPS walking and 19.0 TPS exploring at full speed. Vanilla's watchdog
|
||||
stopped it in both.
|
||||
- **Memory:** Horizon used 0.76–0.92 GB in every walk and equal-work run, and 1.36 GB at most when 100 bots
|
||||
explored at full speed. Vanilla used 3.4–4.0 GB with its 4 GB heap.
|
||||
- **Start:** Horizon's fresh world was ready in 9.2–9.9 s, vanilla's in 14.9–17.2 s.
|
||||
|
||||
## Where Horizon's time goes (50 walking bots)
|
||||
|
||||
`perf record -e cycles:u --call-graph lbr` on the engine for 30 s, during a separate 50-bot `walk` run on the same
|
||||
build and machine (MSPT 21.7 ms, 178 % CPU). Shares of all the engine's CPU samples:
|
||||
|
||||
| Thread | Share | Mostly |
|
||||
|---|---:|---|
|
||||
| Connection writers (`horizon-lan-out`) | 55 % | 53 % in `miniz_oxide` deflate, from `horizon_protocol::frame::encode` |
|
||||
| World generation workers | 33 % | noise, surface rules, aquifers |
|
||||
| Server thread (the tick) | 12 % | see below |
|
||||
|
||||
Inside the tick:
|
||||
- the locator bar (`Waypoints::tick`): a third of the tick;
|
||||
- `Hub::tick`: a quarter;
|
||||
- block entities, creatures, chunk encoding (`ChunkEncoder::encode_in`) and `Streaming::update`: 7–10 % each;
|
||||
- SipHash of `u128` keys (the default `HashMap` hasher): about 10 % of the tick.
|
||||
|
||||
**The next biggest cost is compressing packets.** Every chunk packet is compressed again for each player who gets
|
||||
it, with `flate2`'s default `miniz_oxide` backend at level 6. On this 4-thread CPU that competes with the tick
|
||||
and with world generation. Two fixes, which can be combined:
|
||||
- the `zlib-rs` backend for `flate2`: 2.2–10 times faster than `miniz_oxide` at level 6 in a quick test, with
|
||||
output about 3 % larger;
|
||||
- compressing a chunk packet once and sending the same bytes to every player who needs it.
|
||||
|
||||
## Before and after the performance PRs
|
||||
|
||||
The earlier run, on `main` at `002f14ce` with #670's bench changes (build `a9ee56a8`), before #586, #701–#707 and
|
||||
#713. Same machines and settings. MSPT mean / P95, in ms:
|
||||
|
||||
Walk:
|
||||
|
||||
| Bots | Horizon before | Horizon now | Vanilla before | Vanilla now |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | 5.77 / 11.04 | 4.01 / 8.19 | 18.84 / 29.55 | 18.22 / 29.34 |
|
||||
| 10 | 16.23 / 29.32 | 8.79 / 17.35 | 18.02 / 37.62 | 18.67 / 38.26 |
|
||||
| 25 | 23.98 / 49.18 | 15.01 / 31.12 | 19.19 / 33.84 | 18.78 / 28.16 |
|
||||
| 50 | 35.07 / 73.24 | 22.72 / 60.19 | 35.23 / 60.87 | 34.3 / 55.55 |
|
||||
| 100 | 104.8 / 158.8 | 63.33 / 125.04 | watchdog | watchdog |
|
||||
|
||||
Equal work:
|
||||
|
||||
| Bots | Horizon before | Horizon now | Vanilla before | Vanilla now |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | 4.78 / 7.3 | 3.16 / 5.78 | 16.63 / 26.23 | 16.82 / 27.3 |
|
||||
| 10 | 15.19 / 25.66 | 9.83 / 19.01 | 24.65 / 40.81 | 24.38 / 39.58 |
|
||||
| 25 | 23.37 / 42.59 | 16.87 / 30.88 | 30.28 / 44.81 | 30.29 / 43.47 |
|
||||
| 50 | 30.59 / 67.94 | 23.57 / 47.53 | 43.52 / 56.18 | 42.44 / 54.84 |
|
||||
|
||||
Vanilla's numbers repeat within a few percent, so the runs are comparable. In the earlier run, 25 walking bots
|
||||
were faster on vanilla (19.2 against 24.0 ms). A profile then put a third of the tick in advancement updates,
|
||||
which #701 fixed.
|
||||
|
||||
## Files
|
||||
|
||||
- `walk-explore.json`: the `walk` and full-speed `explore` passes. `explore-equal-rate.json`: the equal-work pass.
|
||||
- `load.txt`: both machines' load averages every 30 s during the runs.
|
||||
- `earlier-002f14ce/`: the same files for the earlier run.
|
||||
- The server's tailnet address is replaced by `<server address>` in the JSON.
|
||||
@@ -1,341 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T15:37:03+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "a9ee56a8b143999c9bc7056850ffcd622a9214aa",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50",
|
||||
"behaviour": "explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 180,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0.8,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/explore-equal-rate.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
|
||||
"connect": "<server address>",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.21,
|
||||
"mspt": {
|
||||
"average": 4.78,
|
||||
"p50": 4.59,
|
||||
"p95": 7.3,
|
||||
"p99": 13.53
|
||||
},
|
||||
"mspt_worst_p99": 36.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 759.91,
|
||||
"max": 771.3
|
||||
},
|
||||
"cpu_percent": 24.49,
|
||||
"chunks_per_second": 1.64,
|
||||
"unique_per_second": 1.27,
|
||||
"forgotten_per_second": 1.54,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 841,
|
||||
"chunks_per_bot": {
|
||||
"explore": 841.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 781.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.35,
|
||||
"mspt": {
|
||||
"average": 16.63,
|
||||
"p50": 16.73,
|
||||
"p95": 26.23,
|
||||
"p99": 35.39
|
||||
},
|
||||
"mspt_worst_p99": 68.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3431.12,
|
||||
"max": 3459.2
|
||||
},
|
||||
"cpu_percent": 69.76,
|
||||
"chunks_per_second": 1.66,
|
||||
"unique_per_second": 1.37,
|
||||
"forgotten_per_second": 1.67,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 840,
|
||||
"chunks_per_bot": {
|
||||
"explore": 840.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 780.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 15.19,
|
||||
"p50": 14.45,
|
||||
"p95": 25.66,
|
||||
"p99": 38.89
|
||||
},
|
||||
"mspt_worst_p99": 57.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 859.86,
|
||||
"max": 920.2
|
||||
},
|
||||
"cpu_percent": 84.95,
|
||||
"chunks_per_second": 14.22,
|
||||
"unique_per_second": 12.7,
|
||||
"forgotten_per_second": 14.11,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 7962,
|
||||
"chunks_per_bot": {
|
||||
"explore": 796.2
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 768.2
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.74,
|
||||
"mspt": {
|
||||
"average": 24.65,
|
||||
"p50": 21.82,
|
||||
"p95": 40.81,
|
||||
"p99": 70.23
|
||||
},
|
||||
"mspt_worst_p99": 119.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3528.77,
|
||||
"max": 3643.2
|
||||
},
|
||||
"cpu_percent": 133.1,
|
||||
"chunks_per_second": 13.62,
|
||||
"unique_per_second": 12.32,
|
||||
"forgotten_per_second": 13.62,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 7746,
|
||||
"chunks_per_bot": {
|
||||
"explore": 774.6
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 748.7
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 1150,
|
||||
"moved_too_quickly": 5,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.42,
|
||||
"mspt": {
|
||||
"average": 23.37,
|
||||
"p50": 21.61,
|
||||
"p95": 42.59,
|
||||
"p99": 60.21
|
||||
},
|
||||
"mspt_worst_p99": 102.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 889.21,
|
||||
"max": 950.7
|
||||
},
|
||||
"cpu_percent": 117.59,
|
||||
"chunks_per_second": 35.34,
|
||||
"unique_per_second": 31.62,
|
||||
"forgotten_per_second": 35.12,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 20137,
|
||||
"chunks_per_bot": {
|
||||
"explore": 805.48
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 774.52
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.14,
|
||||
"mspt": {
|
||||
"average": 30.28,
|
||||
"p50": 28.17,
|
||||
"p95": 44.81,
|
||||
"p99": 71.77
|
||||
},
|
||||
"mspt_worst_p99": 158.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3574.32,
|
||||
"max": 3692.7
|
||||
},
|
||||
"cpu_percent": 173.43,
|
||||
"chunks_per_second": 32.76,
|
||||
"unique_per_second": 29.38,
|
||||
"forgotten_per_second": 33.02,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 19417,
|
||||
"chunks_per_bot": {
|
||||
"explore": 776.68
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 747.36
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 4728,
|
||||
"moved_too_quickly": 15,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.11,
|
||||
"mspt": {
|
||||
"average": 30.59,
|
||||
"p50": 25.19,
|
||||
"p95": 67.94,
|
||||
"p99": 90.77
|
||||
},
|
||||
"mspt_worst_p99": 122.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 915.93,
|
||||
"max": 978.0
|
||||
},
|
||||
"cpu_percent": 132.13,
|
||||
"chunks_per_second": 71.31,
|
||||
"unique_per_second": 63.56,
|
||||
"forgotten_per_second": 71.19,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 41365,
|
||||
"chunks_per_bot": {
|
||||
"explore": 827.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 793.14
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.35,
|
||||
"mspt": {
|
||||
"average": 43.52,
|
||||
"p50": 41.69,
|
||||
"p95": 56.18,
|
||||
"p99": 84.55
|
||||
},
|
||||
"mspt_worst_p99": 155.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3801.29,
|
||||
"max": 3920.7
|
||||
},
|
||||
"cpu_percent": 198.97,
|
||||
"chunks_per_second": 65.86,
|
||||
"unique_per_second": 58.63,
|
||||
"forgotten_per_second": 66.12,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 39790,
|
||||
"chunks_per_bot": {
|
||||
"explore": 795.8
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 764.48
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 8392,
|
||||
"moved_too_quickly": 38,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
2026-10-17T13:17:13Z thinkpad: 2.76 2.26 2.04 | macmini (bots): { 73.08 70.73 75.98 }
|
||||
2026-10-17T13:17:43Z thinkpad: 2.90 2.33 2.07 | macmini (bots): { 75.66 71.62 76.12 }
|
||||
2026-10-17T13:18:13Z thinkpad: 3.02 2.43 2.11 | macmini (bots): { 65.77 69.61 75.24 }
|
||||
2026-10-17T13:18:44Z thinkpad: 2.15 2.27 2.07 | macmini (bots): { 59.85 67.75 74.37 }
|
||||
2026-10-17T13:19:14Z thinkpad: 1.45 2.09 2.02 | macmini (bots): { 63.86 67.87 74.17 }
|
||||
2026-10-17T13:19:44Z thinkpad: 1.28 1.98 1.98 | macmini (bots): { 63.51 67.54 73.84 }
|
||||
2026-10-17T13:20:14Z thinkpad: 1.14 1.89 1.95 | macmini (bots): { 65.82 67.85 73.71 }
|
||||
2026-10-17T13:20:45Z thinkpad: 0.90 1.77 1.91 | macmini (bots): { 63.62 67.08 73.22 }
|
||||
2026-10-17T13:21:15Z thinkpad: 0.55 1.60 1.85 | macmini (bots): { 62.45 66.37 72.75 }
|
||||
2026-10-17T13:21:45Z thinkpad: 0.79 1.54 1.82 | macmini (bots): { 67.04 67.04 72.76 }
|
||||
2026-10-17T13:22:15Z thinkpad: 1.96 1.75 1.88 | macmini (bots): { 65.53 66.60 72.40 }
|
||||
2026-10-17T13:22:46Z thinkpad: 1.82 1.75 1.88 | macmini (bots): { 61.52 65.55 71.82 }
|
||||
2026-10-17T13:23:16Z thinkpad: 1.29 1.63 1.83 | macmini (bots): { 62.36 65.36 71.53 }
|
||||
2026-10-17T13:23:46Z thinkpad: 0.86 1.49 1.78 | macmini (bots): { 68.76 66.51 71.73 }
|
||||
2026-10-17T13:24:16Z thinkpad: 1.29 1.52 1.78 | macmini (bots): { 77.68 68.77 72.36 }
|
||||
2026-10-17T13:24:46Z thinkpad: 1.82 1.62 1.80 | macmini (bots): { 74.03 68.88 72.28 }
|
||||
2026-10-17T13:25:17Z thinkpad: 1.50 1.56 1.78 | macmini (bots): { 71.53 68.76 72.12 }
|
||||
2026-10-17T13:25:47Z thinkpad: 0.96 1.42 1.72 | macmini (bots): { 74.46 69.85 72.40 }
|
||||
2026-10-17T13:26:17Z thinkpad: 0.87 1.35 1.69 | macmini (bots): { 80.85 71.79 73.01 }
|
||||
2026-10-17T13:26:47Z thinkpad: 3.95 2.02 1.90 | macmini (bots): { 74.77 71.31 72.81 }
|
||||
2026-10-17T13:27:18Z thinkpad: 2.98 1.97 1.89 | macmini (bots): { 70.52 70.52 72.46 }
|
||||
2026-10-17T13:27:48Z thinkpad: 3.78 2.26 1.99 | macmini (bots): { 67.62 69.91 72.18 }
|
||||
2026-10-17T13:28:18Z thinkpad: 3.05 2.23 1.99 | macmini (bots): { 72.51 70.83 72.43 }
|
||||
2026-10-17T13:28:48Z thinkpad: 2.05 2.07 1.94 | macmini (bots): { 79.07 72.64 73.03 }
|
||||
2026-10-17T13:29:19Z thinkpad: 2.09 2.11 1.96 | macmini (bots): { 90.42 75.94 74.21 }
|
||||
2026-10-17T13:29:49Z thinkpad: 1.61 1.99 1.93 | macmini (bots): { 102.68 80.15 75.78 }
|
||||
2026-10-17T13:30:19Z thinkpad: 2.31 2.12 1.97 | macmini (bots): { 111.15 84.19 77.39 }
|
||||
2026-10-17T13:30:49Z thinkpad: 4.14 2.61 2.14 | macmini (bots): { 115.67 87.82 78.94 }
|
||||
2026-10-17T13:31:20Z thinkpad: 4.84 2.92 2.26 | macmini (bots): { 122.95 92.31 80.88 }
|
||||
2026-10-17T13:31:50Z thinkpad: 6.84 3.60 2.51 | macmini (bots): { 136.25 98.41 83.48 }
|
||||
2026-10-17T13:32:20Z thinkpad: 4.85 3.43 2.49 | macmini (bots): { 128.59 100.39 84.73 }
|
||||
2026-10-17T13:32:50Z thinkpad: 3.48 3.25 2.46 | macmini (bots): { 111.05 98.73 84.66 }
|
||||
2026-10-17T13:33:20Z thinkpad: 3.09 3.15 2.45 | macmini (bots): { 102.40 98.03 84.98 }
|
||||
2026-10-17T13:33:51Z thinkpad: 2.47 2.99 2.42 | macmini (bots): { 94.57 96.54 84.89 }
|
||||
2026-10-17T13:34:21Z thinkpad: 2.42 2.94 2.42 | macmini (bots): { 86.26 94.36 84.50 }
|
||||
2026-10-17T13:34:51Z thinkpad: 2.95 3.04 2.47 | macmini (bots): { 76.90 91.56 83.84 }
|
||||
2026-10-17T13:35:21Z thinkpad: 2.66 2.96 2.46 | macmini (bots): { 65.57 87.42 82.61 }
|
||||
2026-10-17T13:35:52Z thinkpad: 2.47 2.88 2.45 | macmini (bots): { 64.13 85.05 81.92 }
|
||||
2026-10-17T13:36:22Z thinkpad: 3.89 3.17 2.56 | macmini (bots): { 58.65 81.74 80.83 }
|
||||
2026-10-17T13:36:52Z thinkpad: 6.43 3.83 2.80 | macmini (bots): { 66.32 81.32 80.71 }
|
||||
2026-10-17T13:37:22Z thinkpad: 8.73 4.60 3.09 | macmini (bots): { 67.30 80.14 80.30 }
|
||||
2026-10-17T13:37:53Z thinkpad: 9.03 5.10 3.30 | macmini (bots): { 63.66 78.24 79.62 }
|
||||
2026-10-17T13:38:23Z thinkpad: 9.49 5.57 3.52 | macmini (bots): { 61.25 76.23 78.84 }
|
||||
2026-10-17T13:38:53Z thinkpad: 7.55 5.50 3.56 | macmini (bots): { 62.30 74.88 78.25 }
|
||||
2026-10-17T13:39:23Z thinkpad: 8.62 5.99 3.79 | macmini (bots): { 57.34 72.56 77.29 }
|
||||
2026-10-17T13:39:53Z thinkpad: 6.65 5.81 3.80 | macmini (bots): { 51.24 69.62 76.06 }
|
||||
2026-10-17T13:40:24Z thinkpad: 5.55 5.62 3.80 | macmini (bots): { 48.87 67.26 74.98 }
|
||||
2026-10-17T13:40:54Z thinkpad: 6.11 5.71 3.89 | macmini (bots): { 51.16 66.08 74.29 }
|
||||
2026-10-17T13:41:24Z thinkpad: 8.93 6.43 4.19 | macmini (bots): { 48.67 64.00 73.24 }
|
||||
2026-10-17T13:41:54Z thinkpad: 9.05 6.74 4.36 | macmini (bots): { 46.47 61.96 72.18 }
|
||||
2026-10-17T13:42:25Z thinkpad: 7.70 6.65 4.41 | macmini (bots): { 44.24 60.05 71.14 }
|
||||
2026-10-17T13:42:55Z thinkpad: 5.64 6.24 4.34 | macmini (bots): { 49.30 59.83 70.68 }
|
||||
2026-10-17T13:43:25Z thinkpad: 4.83 6.02 4.33 | macmini (bots): { 47.39 58.24 69.72 }
|
||||
2026-10-17T13:43:55Z thinkpad: 3.57 5.61 4.25 | macmini (bots): { 47.32 57.23 68.96 }
|
||||
2026-10-17T13:44:25Z thinkpad: 3.41 5.40 4.23 | macmini (bots): { 47.86 56.49 68.28 }
|
||||
2026-10-17T13:44:55Z thinkpad: 3.37 5.21 4.20 | macmini (bots): { 50.10 56.19 67.77 }
|
||||
2026-10-17T13:45:26Z thinkpad: 3.78 5.16 4.22 | macmini (bots): { 51.91 55.98 67.28 }
|
||||
2026-10-17T13:45:56Z thinkpad: 4.34 5.12 4.23 | macmini (bots): { 49.16 54.85 66.42 }
|
||||
2026-10-17T13:46:26Z thinkpad: 10.04 6.46 4.71 | macmini (bots): { 45.01 53.33 65.47 }
|
||||
2026-10-17T13:46:56Z thinkpad: 18.10 8.73 5.52 | macmini (bots): { 46.41 52.87 64.88 }
|
||||
2026-10-17T13:47:27Z thinkpad: 14.99 8.91 5.68 | macmini (bots): { 51.12 53.21 64.57 }
|
||||
2026-10-17T13:47:57Z thinkpad: 13.46 9.27 5.92 | macmini (bots): { 53.84 53.81 64.40 }
|
||||
2026-10-17T13:48:27Z thinkpad: 12.76 9.41 6.07 | macmini (bots): { 51.94 53.27 63.84 }
|
||||
2026-10-17T13:48:58Z thinkpad: 10.20 9.11 6.08 | macmini (bots): { 50.70 52.83 63.31 }
|
||||
2026-10-17T13:49:28Z thinkpad: 9.95 9.25 6.23 | macmini (bots): { 51.51 52.89 62.97 }
|
||||
2026-10-17T13:49:58Z thinkpad: 11.98 9.87 6.53 | macmini (bots): { 50.98 52.73 62.57 }
|
||||
2026-10-17T13:50:29Z thinkpad: 14.96 10.76 6.94 | macmini (bots): { 49.68 52.31 62.08 }
|
||||
2026-10-17T13:50:59Z thinkpad: 10.54 10.09 6.83 | macmini (bots): { 53.34 53.01 62.00 }
|
||||
2026-10-17T13:51:29Z thinkpad: 10.21 10.01 6.91 | macmini (bots): { 51.61 52.69 61.57 }
|
||||
2026-10-17T13:52:00Z thinkpad: 11.86 10.42 7.14 | macmini (bots): { 48.94 51.89 60.97 }
|
||||
2026-10-17T13:52:30Z thinkpad: 10.88 10.33 7.22 | macmini (bots): { 48.46 51.44 60.49 }
|
||||
2026-10-17T13:53:01Z thinkpad: 10.94 10.41 7.35 | macmini (bots): { 54.27 52.41 60.47 }
|
||||
2026-10-17T13:53:31Z thinkpad: 10.77 10.42 7.45 | macmini (bots): { 57.13 53.30 60.51 }
|
||||
2026-10-17T13:54:02Z thinkpad: 10.62 10.40 7.53 | macmini (bots): { 53.78 52.84 60.09 }
|
||||
2026-10-17T13:54:32Z thinkpad: 10.85 10.48 7.66 | macmini (bots): { 52.01 52.51 59.73 }
|
||||
2026-10-17T13:55:02Z thinkpad: 10.36 10.39 7.73 | macmini (bots): { 53.44 52.83 59.59 }
|
||||
2026-10-17T13:55:32Z thinkpad: 8.58 9.95 7.67 | macmini (bots): { 58.66 54.12 59.82 }
|
||||
2026-10-17T13:56:03Z thinkpad: 7.29 9.52 7.60 | macmini (bots): { 60.98 55.05 59.96 }
|
||||
2026-10-17T13:56:33Z thinkpad: 5.02 8.75 7.40 | macmini (bots): { 65.14 56.71 60.39 }
|
||||
2026-10-17T13:57:03Z thinkpad: 5.03 8.38 7.32 | macmini (bots): { 57.51 55.73 59.91 }
|
||||
2026-10-17T13:57:34Z thinkpad: 7.39 8.66 7.45 | macmini (bots): { 52.57 54.73 59.41 }
|
||||
2026-10-17T13:58:04Z thinkpad: 10.56 9.45 7.76 | macmini (bots): { 53.11 54.60 59.20 }
|
||||
2026-10-17T13:58:34Z thinkpad: 28.87 14.20 9.41 | macmini (bots): { 54.53 54.93 59.16 }
|
||||
2026-10-17T13:59:05Z thinkpad: 39.33 17.79 10.73 | macmini (bots): { 62.67 56.86 59.71 }
|
||||
2026-10-17T13:59:35Z thinkpad: 34.07 18.33 11.12 | macmini (bots): { 61.78 57.22 59.75 }
|
||||
2026-10-17T14:00:06Z thinkpad: 34.42 19.72 11.80 | macmini (bots): { 53.10 55.55 59.03 }
|
||||
2026-10-17T14:00:36Z thinkpad: 33.60 20.94 12.46 | macmini (bots): { 48.70 54.23 58.43 }
|
||||
2026-10-17T14:01:07Z thinkpad: 29.29 20.99 12.74 | macmini (bots): { 50.25 54.00 58.20 }
|
||||
2026-10-17T14:01:37Z thinkpad: 20.83 19.79 12.61 | macmini (bots): { 50.48 53.74 57.96 }
|
||||
2026-10-17T14:02:08Z thinkpad: 21.74 20.44 13.11 | macmini (bots): { 48.65 53.04 57.56 }
|
||||
2026-10-17T14:02:38Z thinkpad: 17.68 19.67 13.09 | macmini (bots): { 54.17 53.85 57.69 }
|
||||
2026-10-17T14:03:08Z thinkpad: 13.97 18.55 12.93 | macmini (bots): { 53.39 53.84 57.56 }
|
||||
2026-10-17T14:03:39Z thinkpad: 14.10 18.17 12.98 | macmini (bots): { 50.71 53.09 57.16 }
|
||||
2026-10-17T14:04:09Z thinkpad: 13.60 17.65 12.97 | macmini (bots): { 52.30 53.28 57.08 }
|
||||
2026-10-17T14:04:39Z thinkpad: 12.09 16.91 12.88 | macmini (bots): { 51.52 53.03 56.87 }
|
||||
2026-10-17T14:05:10Z thinkpad: 10.15 15.98 12.69 | macmini (bots): { 54.53 53.57 56.91 }
|
||||
2026-10-17T14:05:42Z thinkpad: 7.77 14.86 12.42 | macmini (bots): { 63.58 55.55 57.49 }
|
||||
2026-10-17T14:06:12Z thinkpad: 6.58 13.89 12.18 | macmini (bots): { 75.29 59.41 58.84 }
|
||||
2026-10-17T14:06:42Z thinkpad: 5.96 13.02 11.94 | macmini (bots): { 78.06 61.64 59.67 }
|
||||
2026-10-17T14:07:12Z thinkpad: 4.86 12.08 11.66 | macmini (bots): { 72.26 61.85 59.81 }
|
||||
2026-10-17T14:07:42Z thinkpad: 4.49 11.18 11.38 | macmini (bots): { 67.84 61.73 59.84 }
|
||||
2026-10-17T14:08:13Z thinkpad: 4.37 10.51 11.15 | macmini (bots): { 64.25 61.45 59.80 }
|
||||
2026-10-17T14:08:43Z thinkpad: 4.57 9.97 10.94 | macmini (bots): { 57.86 60.14 59.38 }
|
||||
2026-10-17T14:09:13Z thinkpad: 4.60 9.46 10.74 | macmini (bots): { 60.21 60.39 59.49 }
|
||||
2026-10-17T14:09:43Z thinkpad: 4.20 8.89 10.51 | macmini (bots): { 57.11 59.57 59.22 }
|
||||
2026-10-17T14:10:14Z thinkpad: 3.80 8.36 10.28 | macmini (bots): { 66.05 61.48 59.92 }
|
||||
2026-10-17T14:10:44Z thinkpad: 4.19 8.00 10.10 | macmini (bots): { 78.51 64.91 61.22 }
|
||||
2026-10-17T14:11:14Z thinkpad: 4.27 7.65 9.92 | macmini (bots): { 74.15 65.33 61.51 }
|
||||
2026-10-17T14:11:44Z thinkpad: 4.35 7.35 9.74 | macmini (bots): { 62.47 63.48 60.98 }
|
||||
2026-10-17T14:12:14Z thinkpad: 4.98 7.22 9.62 | macmini (bots): { 56.73 61.86 60.46 }
|
||||
2026-10-17T14:12:45Z thinkpad: 4.60 6.91 9.44 | macmini (bots): { 57.78 61.49 60.37 }
|
||||
2026-10-17T14:13:15Z thinkpad: 4.15 6.57 9.25 | macmini (bots): { 56.21 60.71 60.12 }
|
||||
2026-10-17T14:13:45Z thinkpad: 3.87 6.25 9.05 | macmini (bots): { 54.75 60.00 59.89 }
|
||||
2026-10-17T14:14:15Z thinkpad: 3.86 6.02 8.89 | macmini (bots): { 52.21 58.88 59.49 }
|
||||
2026-10-17T14:14:46Z thinkpad: 3.14 5.64 8.67 | macmini (bots): { 58.81 59.59 59.71 }
|
||||
2026-10-17T14:15:16Z thinkpad: 3.76 5.54 8.54 | macmini (bots): { 64.13 60.86 60.16 }
|
||||
2026-10-17T14:15:46Z thinkpad: 3.91 5.41 8.40 | macmini (bots): { 59.02 59.97 59.87 }
|
||||
2026-10-17T14:16:16Z thinkpad: 4.33 5.35 8.28 | macmini (bots): { 64.48 60.97 60.22 }
|
||||
2026-10-17T14:16:46Z thinkpad: 5.20 5.44 8.21 | macmini (bots): { 79.56 64.98 61.70 }
|
||||
2026-10-17T14:17:17Z thinkpad: 4.88 5.36 8.10 | macmini (bots): { 94.21 69.99 63.63 }
|
||||
2026-10-17T14:17:47Z thinkpad: 4.80 5.30 8.00 | macmini (bots): { 85.75 70.51 64.08 }
|
||||
2026-10-17T14:18:17Z thinkpad: 4.82 5.25 7.89 | macmini (bots): { 82.33 71.25 64.57 }
|
||||
2026-10-17T14:18:48Z thinkpad: 4.50 5.12 7.75 | macmini (bots): { 77.67 71.11 64.75 }
|
||||
2026-10-17T14:19:18Z thinkpad: 3.76 4.88 7.58 | macmini (bots): { 77.29 71.67 65.17 }
|
||||
2026-10-17T14:19:48Z thinkpad: 3.68 4.75 7.45 | macmini (bots): { 71.16 70.71 65.05 }
|
||||
2026-10-17T14:20:18Z thinkpad: 4.25 4.82 7.39 | macmini (bots): { 66.97 69.63 64.85 }
|
||||
2026-10-17T14:20:49Z thinkpad: 4.72 4.89 7.33 | macmini (bots): { 59.23 67.50 64.24 }
|
||||
2026-10-17T14:21:19Z thinkpad: 5.12 4.98 7.28 | macmini (bots): { 52.38 65.10 63.48 }
|
||||
2026-10-17T14:21:49Z thinkpad: 5.69 5.13 7.26 | macmini (bots): { 59.43 65.53 63.69 }
|
||||
2026-10-17T14:22:19Z thinkpad: 5.78 5.19 7.21 | macmini (bots): { 62.28 65.65 63.80 }
|
||||
2026-10-17T14:22:50Z thinkpad: 5.34 5.14 7.13 | macmini (bots): { 57.40 64.12 63.30 }
|
||||
2026-10-17T14:23:20Z thinkpad: 4.89 5.04 7.03 | macmini (bots): { 58.39 63.61 63.14 }
|
||||
2026-10-17T14:23:50Z thinkpad: 5.00 5.07 6.98 | macmini (bots): { 64.88 64.63 63.52 }
|
||||
2026-10-17T14:24:20Z thinkpad: 3.88 4.80 6.83 | macmini (bots): { 80.70 68.30 64.88 }
|
||||
2026-10-17T14:24:51Z thinkpad: 2.90 4.47 6.65 | macmini (bots): { 94.09 72.75 66.61 }
|
||||
2026-10-17T14:25:21Z thinkpad: 2.63 4.25 6.51 | macmini (bots): { 93.13 74.91 67.63 }
|
||||
2026-10-17T14:25:51Z thinkpad: 6.66 5.07 6.71 | macmini (bots): { 72.87 71.72 66.72 }
|
||||
2026-10-17T14:26:21Z thinkpad: 7.15 5.34 6.74 | macmini (bots): { 67.03 70.37 66.40 }
|
||||
2026-10-17T14:26:51Z thinkpad: 6.20 5.29 6.68 | macmini (bots): { 58.68 68.08 65.71 }
|
||||
2026-10-17T14:27:22Z thinkpad: 6.11 5.35 6.66 | macmini (bots): { 55.26 66.28 65.14 }
|
||||
2026-10-17T14:27:52Z thinkpad: 5.45 5.26 6.59 | macmini (bots): { 56.51 65.74 64.99 }
|
||||
2026-10-17T14:28:22Z thinkpad: 6.11 5.45 6.61 | macmini (bots): { 52.24 63.77 64.30 }
|
||||
2026-10-17T14:28:52Z thinkpad: 6.06 5.50 6.59 | macmini (bots): { 55.56 63.38 64.13 }
|
||||
2026-10-17T14:29:23Z thinkpad: 6.11 5.58 6.58 | macmini (bots): { 59.66 63.63 64.20 }
|
||||
2026-10-17T14:29:53Z thinkpad: 5.52 5.54 6.53 | macmini (bots): { 63.71 64.30 64.42 }
|
||||
2026-10-17T14:30:23Z thinkpad: 4.79 5.35 6.43 | macmini (bots): { 69.84 65.61 64.89 }
|
||||
2026-10-17T14:30:53Z thinkpad: 6.21 5.63 6.49 | macmini (bots): { 62.76 64.46 64.50 }
|
||||
2026-10-17T14:31:23Z thinkpad: 8.36 6.21 6.66 | macmini (bots): { 53.51 62.09 63.64 }
|
||||
2026-10-17T14:31:54Z thinkpad: 8.49 6.45 6.72 | macmini (bots): { 52.80 60.98 63.18 }
|
||||
2026-10-17T14:32:24Z thinkpad: 8.28 6.59 6.77 | macmini (bots): { 48.98 59.20 62.45 }
|
||||
2026-10-17T14:32:54Z thinkpad: 8.02 6.70 6.79 | macmini (bots): { 48.95 58.30 62.02 }
|
||||
2026-10-17T14:33:25Z thinkpad: 7.02 6.59 6.76 | macmini (bots): { 51.06 57.70 61.66 }
|
||||
2026-10-17T14:33:55Z thinkpad: 7.09 6.62 6.76 | macmini (bots): { 70.89 61.81 63.00 }
|
||||
2026-10-17T14:34:25Z thinkpad: 6.18 6.44 6.69 | macmini (bots): { 69.38 62.46 63.21 }
|
||||
2026-10-17T14:34:55Z thinkpad: 6.67 6.52 6.72 | macmini (bots): { 76.87 64.84 64.04 }
|
||||
2026-10-17T14:35:25Z thinkpad: 4.76 6.08 6.56 | macmini (bots): { 86.19 68.04 65.21 }
|
||||
2026-10-17T14:35:56Z thinkpad: 3.47 5.64 6.40 | macmini (bots): { 101.03 73.28 67.21 }
|
||||
2026-10-17T14:36:26Z thinkpad: 2.68 5.24 6.24 | macmini (bots): { 106.05 77.09 68.79 }
|
||||
2026-10-17T14:36:56Z thinkpad: 2.41 4.91 6.10 | macmini (bots): { 107.57 80.20 70.20 }
|
||||
2026-10-17T14:37:26Z thinkpad: 3.74 4.99 6.08 | macmini (bots): { 101.16 81.38 70.98 }
|
||||
2026-10-17T14:37:57Z thinkpad: 5.96 5.39 6.18 | macmini (bots): { 86.51 79.61 70.70 }
|
||||
2026-10-17T14:38:27Z thinkpad: 7.91 5.98 6.35 | macmini (bots): { 81.13 79.06 70.81 }
|
||||
2026-10-17T14:38:57Z thinkpad: 8.45 6.25 6.43 | macmini (bots): { 79.56 79.01 71.08 }
|
||||
2026-10-17T14:39:27Z thinkpad: 8.75 6.52 6.52 | macmini (bots): { 71.92 77.22 70.71 }
|
||||
2026-10-17T14:39:58Z thinkpad: 8.86 6.76 6.60 | macmini (bots): { 71.38 76.58 70.70 }
|
||||
2026-10-17T14:40:28Z thinkpad: 8.74 6.98 6.68 | macmini (bots): { 71.25 76.12 70.74 }
|
||||
2026-10-17T14:40:58Z thinkpad: 7.99 6.96 6.68 | macmini (bots): { 70.49 75.51 70.71 }
|
||||
2026-10-17T14:41:28Z thinkpad: 7.64 7.01 6.70 | macmini (bots): { 68.93 74.58 70.56 }
|
||||
2026-10-17T14:41:59Z thinkpad: 6.57 6.84 6.66 | macmini (bots): { 68.35 73.75 70.39 }
|
||||
2026-10-17T14:42:29Z thinkpad: 5.83 6.62 6.59 | macmini (bots): { 63.57 72.20 69.95 }
|
||||
2026-10-17T14:42:59Z thinkpad: 7.40 6.91 6.69 | macmini (bots): { 56.77 69.69 69.12 }
|
||||
2026-10-17T14:43:29Z thinkpad: 9.07 7.37 6.85 | macmini (bots): { 49.53 66.71 68.06 }
|
||||
2026-10-17T14:44:00Z thinkpad: 9.39 7.63 6.96 | macmini (bots): { 43.53 63.68 66.91 }
|
||||
2026-10-17T14:44:30Z thinkpad: 9.16 7.73 7.01 | macmini (bots): { 41.12 61.23 65.92 }
|
||||
2026-10-17T14:45:00Z thinkpad: 7.96 7.57 6.98 | macmini (bots): { 40.62 59.23 65.03 }
|
||||
2026-10-17T14:45:30Z thinkpad: 6.34 7.24 6.89 | macmini (bots): { 38.17 56.80 63.94 }
|
||||
2026-10-17T14:46:01Z thinkpad: 6.39 7.18 6.88 | macmini (bots): { 40.35 55.54 63.24 }
|
||||
2026-10-17T14:46:31Z thinkpad: 5.63 6.95 6.82 | macmini (bots): { 48.66 56.25 63.24 }
|
||||
2026-10-17T14:47:01Z thinkpad: 4.73 6.60 6.70 | macmini (bots): { 46.68 55.20 62.62 }
|
||||
2026-10-17T14:47:31Z thinkpad: 9.18 7.55 7.02 | macmini (bots): { 41.45 53.19 61.64 }
|
||||
2026-10-17T14:48:01Z thinkpad: 10.40 8.02 7.19 | macmini (bots): { 40.76 51.87 60.87 }
|
||||
2026-10-17T14:48:32Z thinkpad: 8.24 7.73 7.12 | macmini (bots): { 44.46 51.67 60.48 }
|
||||
2026-10-17T14:49:02Z thinkpad: 9.60 8.09 7.26 | macmini (bots): { 38.75 49.66 59.45 }
|
||||
2026-10-17T14:49:32Z thinkpad: 8.90 8.10 7.29 | macmini (bots): { 34.93 47.72 58.41 }
|
||||
2026-10-17T14:50:02Z thinkpad: 11.17 8.66 7.50 | macmini (bots): { 35.70 46.68 57.66 }
|
||||
2026-10-17T14:50:33Z thinkpad: 12.06 9.19 7.72 | macmini (bots): { 43.94 47.43 57.54 }
|
||||
2026-10-17T14:51:03Z thinkpad: 12.50 9.49 7.87 | macmini (bots): { 40.76 46.36 56.80 }
|
||||
2026-10-17T14:51:33Z thinkpad: 10.35 9.24 7.83 | macmini (bots): { 44.81 46.69 56.55 }
|
||||
2026-10-17T14:52:03Z thinkpad: 9.32 9.09 7.83 | macmini (bots): { 54.94 48.97 57.04 }
|
||||
2026-10-17T14:52:34Z thinkpad: 7.07 8.58 7.70 | macmini (bots): { 54.59 49.54 56.92 }
|
||||
2026-10-17T14:53:04Z thinkpad: 5.33 8.01 7.54 | macmini (bots): { 67.48 53.27 58.02 }
|
||||
2026-10-17T14:53:34Z thinkpad: 7.22 8.20 7.62 | macmini (bots): { 57.82 52.33 57.52 }
|
||||
2026-10-17T14:54:04Z thinkpad: 9.69 8.73 7.81 | macmini (bots): { 46.67 50.20 56.57 }
|
||||
2026-10-17T14:54:35Z thinkpad: 9.14 8.66 7.82 | macmini (bots): { 49.88 50.61 56.49 }
|
||||
2026-10-17T14:55:05Z thinkpad: 9.75 8.87 7.91 | macmini (bots): { 46.88 49.95 56.06 }
|
||||
2026-10-17T14:55:35Z thinkpad: 8.51 8.67 7.88 | macmini (bots): { 42.90 48.80 55.43 }
|
||||
2026-10-17T14:56:06Z thinkpad: 8.46 8.62 7.89 | macmini (bots): { 49.23 49.72 55.53 }
|
||||
2026-10-17T14:56:36Z thinkpad: 6.19 8.07 7.73 | macmini (bots): { 54.41 50.86 55.74 }
|
||||
2026-10-17T14:57:06Z thinkpad: 4.69 7.53 7.56 | macmini (bots): { 60.26 52.69 56.24 }
|
||||
2026-10-17T14:57:36Z thinkpad: 3.21 6.90 7.35 | macmini (bots): { 67.66 55.21 57.03 }
|
||||
2026-10-17T14:58:06Z thinkpad: 1.94 6.24 7.11 | macmini (bots): { 75.73 58.41 58.13 }
|
||||
2026-10-17T14:58:37Z thinkpad: 1.18 5.64 6.89 | macmini (bots): { 74.69 59.90 58.68 }
|
||||
2026-10-17T14:59:07Z thinkpad: 0.78 5.12 6.67 | macmini (bots): { 77.10 61.82 59.42 }
|
||||
2026-10-17T14:59:37Z thinkpad: 0.88 4.72 6.49 | macmini (bots): { 87.05 65.59 60.86 }
|
||||
2026-10-17T15:00:07Z thinkpad: 0.53 4.27 6.28 | macmini (bots): { 78.18 65.51 60.99 }
|
||||
2026-10-17T15:00:38Z thinkpad: 0.38 3.88 6.09 | macmini (bots): { 79.38 67.00 61.69 }
|
||||
2026-10-17T15:01:08Z thinkpad: 1.50 3.80 5.99 | macmini (bots): { 78.36 67.86 62.18 }
|
||||
2026-10-17T15:01:38Z thinkpad: 2.97 3.93 5.95 | macmini (bots): { 74.18 67.90 62.39 }
|
||||
2026-10-17T15:02:08Z thinkpad: 2.61 3.77 5.83 | macmini (bots): { 68.63 67.00 62.25 }
|
||||
2026-10-17T15:02:39Z thinkpad: 2.10 3.55 5.69 | macmini (bots): { 69.84 67.50 62.60 }
|
||||
2026-10-17T15:03:09Z thinkpad: 1.44 3.25 5.53 | macmini (bots): { 67.10 67.06 62.61 }
|
||||
2026-10-17T15:03:39Z thinkpad: 1.39 3.08 5.40 | macmini (bots): { 60.83 65.50 62.19 }
|
||||
2026-10-17T15:04:09Z thinkpad: 1.28 2.88 5.26 | macmini (bots): { 75.40 68.61 63.46 }
|
||||
2026-10-17T15:04:40Z thinkpad: 2.30 2.99 5.22 | macmini (bots): { 106.08 76.54 66.50 }
|
||||
2026-10-17T15:05:10Z thinkpad: 2.65 3.02 5.15 | macmini (bots): { 111.96 81.09 68.52 }
|
||||
2026-10-17T15:05:40Z thinkpad: 2.41 2.92 5.05 | macmini (bots): { 104.19 82.08 69.31 }
|
||||
2026-10-17T15:06:10Z thinkpad: 2.71 2.95 4.99 | macmini (bots): { 116.20 87.04 71.54 }
|
||||
2026-10-17T15:06:40Z thinkpad: 2.51 2.87 4.90 | macmini (bots): { 136.68 94.24 74.66 }
|
||||
2026-10-17T15:07:11Z thinkpad: 3.68 3.11 4.91 | macmini (bots): { 132.76 97.43 76.50 }
|
||||
2026-10-17T15:07:41Z thinkpad: 3.77 3.19 4.88 | macmini (bots): { 132.73 100.97 78.51 }
|
||||
2026-10-17T15:08:11Z thinkpad: 3.00 3.06 4.79 | macmini (bots): { 116.92 100.33 79.07 }
|
||||
2026-10-17T15:08:41Z thinkpad: 2.40 2.91 4.68 | macmini (bots): { 109.83 100.07 79.70 }
|
||||
2026-10-17T15:09:12Z thinkpad: 2.18 2.81 4.59 | macmini (bots): { 113.21 101.71 81.00 }
|
||||
2026-10-17T15:09:42Z thinkpad: 1.67 2.62 4.47 | macmini (bots): { 131.87 107.11 83.66 }
|
||||
2026-10-17T15:10:12Z thinkpad: 1.90 2.58 4.39 | macmini (bots): { 133.45 109.92 85.49 }
|
||||
2026-10-17T15:10:42Z thinkpad: 4.00 3.02 4.48 | macmini (bots): { 130.14 111.67 86.99 }
|
||||
2026-10-17T15:11:13Z thinkpad: 4.77 3.32 4.54 | macmini (bots): { 118.78 110.28 87.32 }
|
||||
2026-10-17T15:11:43Z thinkpad: 3.86 3.24 4.47 | macmini (bots): { 124.01 112.36 88.87 }
|
||||
2026-10-17T15:12:13Z thinkpad: 3.61 3.26 4.44 | macmini (bots): { 122.21 112.94 89.88 }
|
||||
2026-10-17T15:12:43Z thinkpad: 3.12 3.17 4.36 | macmini (bots): { 111.62 111.10 90.00 }
|
||||
2026-10-17T15:13:14Z thinkpad: 3.07 3.14 4.31 | macmini (bots): { 122.97 113.95 91.77 }
|
||||
2026-10-17T15:13:44Z thinkpad: 3.33 3.20 4.30 | macmini (bots): { 102.64 109.89 91.07 }
|
||||
2026-10-17T15:14:14Z thinkpad: 2.85 3.10 4.23 | macmini (bots): { 94.46 107.15 90.72 }
|
||||
2026-10-17T15:14:44Z thinkpad: 2.66 3.03 4.17 | macmini (bots): { 94.94 106.07 90.90 }
|
||||
2026-10-17T15:15:14Z thinkpad: 2.54 2.96 4.11 | macmini (bots): { 94.52 104.93 91.01 }
|
||||
2026-10-17T15:15:45Z thinkpad: 3.20 3.08 4.11 | macmini (bots): { 84.00 101.47 90.25 }
|
||||
2026-10-17T15:16:15Z thinkpad: 5.56 3.61 4.25 | macmini (bots): { 83.16 99.76 90.02 }
|
||||
2026-10-17T15:16:45Z thinkpad: 5.66 3.84 4.30 | macmini (bots): { 69.71 94.66 88.56 }
|
||||
2026-10-17T15:17:15Z thinkpad: 5.04 3.90 4.31 | macmini (bots): { 59.31 89.78 87.00 }
|
||||
2026-10-17T15:17:46Z thinkpad: 4.29 3.87 4.29 | macmini (bots): { 58.57 86.47 85.89 }
|
||||
2026-10-17T15:18:16Z thinkpad: 4.16 3.91 4.29 | macmini (bots): { 56.43 83.29 84.75 }
|
||||
2026-10-17T15:18:46Z thinkpad: 2.80 3.60 4.18 | macmini (bots): { 50.60 79.22 83.22 }
|
||||
2026-10-17T15:19:16Z thinkpad: 1.85 3.28 4.05 | macmini (bots): { 54.97 77.50 82.46 }
|
||||
2026-10-17T15:19:47Z thinkpad: 1.82 3.14 3.98 | macmini (bots): { 56.82 75.80 81.67 }
|
||||
2026-10-17T15:20:17Z thinkpad: 2.19 3.10 3.94 | macmini (bots): { 59.30 74.46 80.97 }
|
||||
2026-10-17T15:20:47Z thinkpad: 4.44 3.54 4.06 | macmini (bots): { 55.22 72.11 79.90 }
|
||||
2026-10-17T15:21:17Z thinkpad: 5.99 4.03 4.21 | macmini (bots): { 49.41 69.01 78.50 }
|
||||
2026-10-17T15:21:48Z thinkpad: 4.85 3.95 4.18 | macmini (bots): { 46.89 66.58 77.29 }
|
||||
2026-10-17T15:22:18Z thinkpad: 4.31 3.92 4.16 | macmini (bots): { 45.13 64.31 76.10 }
|
||||
2026-10-17T15:22:48Z thinkpad: 4.28 3.95 4.16 | macmini (bots): { 51.82 63.98 75.56 }
|
||||
2026-10-17T15:23:18Z thinkpad: 4.75 4.11 4.21 | macmini (bots): { 50.35 62.51 74.63 }
|
||||
2026-10-17T15:23:48Z thinkpad: 4.08 4.02 4.17 | macmini (bots): { 44.62 60.01 73.31 }
|
||||
2026-10-17T15:24:19Z thinkpad: 3.93 4.00 4.16 | macmini (bots): { 46.02 58.72 72.37 }
|
||||
2026-10-17T15:24:49Z thinkpad: 3.67 3.92 4.13 | macmini (bots): { 47.23 57.90 71.60 }
|
||||
2026-10-17T15:25:19Z thinkpad: 2.79 3.68 4.04 | macmini (bots): { 50.80 57.78 71.09 }
|
||||
2026-10-17T15:25:49Z thinkpad: 2.79 3.58 3.99 | macmini (bots): { 47.66 56.35 70.11 }
|
||||
2026-10-17T15:26:19Z thinkpad: 5.04 3.97 4.11 | macmini (bots): { 45.15 54.84 69.08 }
|
||||
2026-10-17T15:26:50Z thinkpad: 8.46 4.86 4.40 | macmini (bots): { 43.59 53.51 68.10 }
|
||||
2026-10-17T15:27:20Z thinkpad: 6.84 4.81 4.39 | macmini (bots): { 42.38 52.39 67.19 }
|
||||
2026-10-17T15:27:50Z thinkpad: 8.24 5.33 4.58 | macmini (bots): { 49.87 53.02 66.82 }
|
||||
2026-10-17T15:28:21Z thinkpad: 7.11 5.34 4.61 | macmini (bots): { 61.30 55.60 67.28 }
|
||||
2026-10-17T15:28:51Z thinkpad: 6.11 5.27 4.61 | macmini (bots): { 67.60 57.74 67.66 }
|
||||
2026-10-17T15:29:21Z thinkpad: 5.33 5.17 4.59 | macmini (bots): { 63.72 57.79 67.33 }
|
||||
2026-10-17T15:29:51Z thinkpad: 3.57 4.75 4.47 | macmini (bots): { 60.80 57.62 66.94 }
|
||||
2026-10-17T15:30:22Z thinkpad: 4.74 4.88 4.52 | macmini (bots): { 59.69 57.64 66.62 }
|
||||
2026-10-17T15:30:52Z thinkpad: 3.44 4.54 4.42 | macmini (bots): { 66.89 59.49 66.97 }
|
||||
2026-10-17T15:31:22Z thinkpad: 4.57 4.69 4.47 | macmini (bots): { 83.43 64.16 68.40 }
|
||||
2026-10-17T15:31:52Z thinkpad: 7.17 5.30 4.68 | macmini (bots): { 77.62 64.77 68.49 }
|
||||
2026-10-17T15:32:23Z thinkpad: 8.55 5.82 4.88 | macmini (bots): { 74.20 65.09 68.47 }
|
||||
2026-10-17T15:32:53Z thinkpad: 7.49 5.85 4.92 | macmini (bots): { 82.07 67.70 69.29 }
|
||||
2026-10-17T15:33:23Z thinkpad: 5.65 5.59 4.86 | macmini (bots): { 79.44 68.47 69.51 }
|
||||
2026-10-17T15:33:54Z thinkpad: 5.73 5.64 4.91 | macmini (bots): { 78.94 69.26 69.76 }
|
||||
2026-10-17T15:34:24Z thinkpad: 6.84 5.92 5.02 | macmini (bots): { 76.33 69.71 69.91 }
|
||||
2026-10-17T15:34:54Z thinkpad: 5.11 5.59 4.94 | macmini (bots): { 86.09 72.63 70.95 }
|
||||
2026-10-17T15:35:24Z thinkpad: 6.68 5.93 5.08 | macmini (bots): { 72.77 70.70 70.31 }
|
||||
2026-10-17T15:35:55Z thinkpad: 5.75 5.75 5.04 | macmini (bots): { 71.50 70.49 70.24 }
|
||||
2026-10-17T15:36:25Z thinkpad: 4.31 5.41 4.95 | macmini (bots): { 80.75 72.67 71.03 }
|
||||
2026-10-17T15:36:55Z thinkpad: 3.15 5.02 4.83 | macmini (bots): { 89.83 75.59 72.14 }
|
||||
@@ -1,695 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T14:56:24+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "a9ee56a8b143999c9bc7056850ffcd622a9214aa",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50,100",
|
||||
"behaviour": "walk,explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 180,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/walk-explore.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
|
||||
"connect": "<server address>",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.51,
|
||||
"mspt": {
|
||||
"average": 5.77,
|
||||
"p50": 5.15,
|
||||
"p95": 11.04,
|
||||
"p99": 28.99
|
||||
},
|
||||
"mspt_worst_p99": 45.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 777.78,
|
||||
"max": 779.8
|
||||
},
|
||||
"cpu_percent": 59.02,
|
||||
"chunks_per_second": 9.94,
|
||||
"unique_per_second": 0.45,
|
||||
"forgotten_per_second": 9.99,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 2763,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2763.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 697.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.35,
|
||||
"mspt": {
|
||||
"average": 18.84,
|
||||
"p50": 18.87,
|
||||
"p95": 29.55,
|
||||
"p99": 51.32
|
||||
},
|
||||
"mspt_worst_p99": 456.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3442.06,
|
||||
"max": 3462.1
|
||||
},
|
||||
"cpu_percent": 70.63,
|
||||
"chunks_per_second": 10.47,
|
||||
"unique_per_second": 0.47,
|
||||
"forgotten_per_second": 10.53,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 2763,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2763.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 697.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 16.23,
|
||||
"p50": 15.6,
|
||||
"p95": 29.32,
|
||||
"p99": 44.43
|
||||
},
|
||||
"mspt_worst_p99": 100.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 781.84,
|
||||
"max": 782.6
|
||||
},
|
||||
"cpu_percent": 136.59,
|
||||
"chunks_per_second": 93.82,
|
||||
"unique_per_second": 3.33,
|
||||
"forgotten_per_second": 93.61,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 26481,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2648.1
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 679.5
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.65,
|
||||
"mspt": {
|
||||
"average": 18.02,
|
||||
"p50": 15.22,
|
||||
"p95": 37.62,
|
||||
"p99": 54.5
|
||||
},
|
||||
"mspt_worst_p99": 73.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3408.62,
|
||||
"max": 3423.6
|
||||
},
|
||||
"cpu_percent": 100.57,
|
||||
"chunks_per_second": 101.12,
|
||||
"unique_per_second": 4.25,
|
||||
"forgotten_per_second": 101.17,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 26763,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2676.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 687.8
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 7,
|
||||
"moved_too_quickly": 7,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 23.98,
|
||||
"p50": 20.89,
|
||||
"p95": 49.18,
|
||||
"p99": 65.87
|
||||
},
|
||||
"mspt_worst_p99": 109.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 786.18,
|
||||
"max": 786.5
|
||||
},
|
||||
"cpu_percent": 159.81,
|
||||
"chunks_per_second": 235.75,
|
||||
"unique_per_second": 7.84,
|
||||
"forgotten_per_second": 235.72,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 67251,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2690.04
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 683.92
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.95,
|
||||
"mspt": {
|
||||
"average": 19.19,
|
||||
"p50": 17.34,
|
||||
"p95": 33.84,
|
||||
"p99": 56.21
|
||||
},
|
||||
"mspt_worst_p99": 79.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3531.46,
|
||||
"max": 3582.1
|
||||
},
|
||||
"cpu_percent": 153.78,
|
||||
"chunks_per_second": 252.35,
|
||||
"unique_per_second": 11.73,
|
||||
"forgotten_per_second": 251.58,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 66759,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2670.36
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 689.76
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 12,
|
||||
"moved_too_quickly": 12,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.21,
|
||||
"mspt": {
|
||||
"average": 35.07,
|
||||
"p50": 30.55,
|
||||
"p95": 73.24,
|
||||
"p99": 99.38
|
||||
},
|
||||
"mspt_worst_p99": 117.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 816.58,
|
||||
"max": 818.5
|
||||
},
|
||||
"cpu_percent": 187.42,
|
||||
"chunks_per_second": 469.11,
|
||||
"unique_per_second": 15.92,
|
||||
"forgotten_per_second": 468.62,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 136962,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2739.24
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 685.68
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.95,
|
||||
"mspt": {
|
||||
"average": 35.23,
|
||||
"p50": 31.39,
|
||||
"p95": 60.87,
|
||||
"p99": 104.2
|
||||
},
|
||||
"mspt_worst_p99": 443.7,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3562.57,
|
||||
"max": 3598.9
|
||||
},
|
||||
"cpu_percent": 263.27,
|
||||
"chunks_per_second": 497.51,
|
||||
"unique_per_second": 30.67,
|
||||
"forgotten_per_second": 495.7,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 131116,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2622.32
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 687.78
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 96,
|
||||
"moved_too_quickly": 96,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.21,
|
||||
"mspt": {
|
||||
"average": 104.8,
|
||||
"p50": 100.6,
|
||||
"p95": 158.8,
|
||||
"p99": 198.61
|
||||
},
|
||||
"mspt_worst_p99": 259.8,
|
||||
"tps": 9.54,
|
||||
"rss_mb": {
|
||||
"mean": 850.05,
|
||||
"max": 851.2
|
||||
},
|
||||
"cpu_percent": 198.95,
|
||||
"chunks_per_second": 371.72,
|
||||
"unique_per_second": 26.27,
|
||||
"forgotten_per_second": 365.57,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 144465,
|
||||
"chunks_per_bot": {
|
||||
"walk": 1444.65
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 571.76
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 34
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.25,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.41,
|
||||
"mspt": {
|
||||
"average": 11.42,
|
||||
"p50": 10.15,
|
||||
"p95": 24.66,
|
||||
"p99": 37.27
|
||||
},
|
||||
"mspt_worst_p99": 60.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 838.61,
|
||||
"max": 888.0
|
||||
},
|
||||
"cpu_percent": 326.85,
|
||||
"chunks_per_second": 41.83,
|
||||
"unique_per_second": 36.39,
|
||||
"forgotten_per_second": 41.75,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 9845,
|
||||
"chunks_per_bot": {
|
||||
"explore": 9845.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 8718.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.44,
|
||||
"mspt": {
|
||||
"average": 9.83,
|
||||
"p50": 7.72,
|
||||
"p95": 20.36,
|
||||
"p99": 84.65
|
||||
},
|
||||
"mspt_worst_p99": 385.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3598.02,
|
||||
"max": 3658.9
|
||||
},
|
||||
"cpu_percent": 244.86,
|
||||
"chunks_per_second": 35.28,
|
||||
"unique_per_second": 33.34,
|
||||
"forgotten_per_second": 35.13,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 7881,
|
||||
"chunks_per_bot": {
|
||||
"explore": 7881.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 7540.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.52,
|
||||
"mspt": {
|
||||
"average": 23.54,
|
||||
"p50": 21.18,
|
||||
"p95": 43.4,
|
||||
"p99": 60.85
|
||||
},
|
||||
"mspt_worst_p99": 109.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1139.51,
|
||||
"max": 1301.0
|
||||
},
|
||||
"cpu_percent": 331.74,
|
||||
"chunks_per_second": 43.86,
|
||||
"unique_per_second": 43.09,
|
||||
"forgotten_per_second": 43.82,
|
||||
"waited_ticks_per_second": 172.35,
|
||||
"chunks_total": 14140,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1414.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1395.9
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.78,
|
||||
"mspt": {
|
||||
"average": 16.42,
|
||||
"p50": 14.01,
|
||||
"p95": 30.29,
|
||||
"p99": 96.17
|
||||
},
|
||||
"mspt_worst_p99": 1144.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3964.49,
|
||||
"max": 4248.3
|
||||
},
|
||||
"cpu_percent": 256.86,
|
||||
"chunks_per_second": 25.43,
|
||||
"unique_per_second": 25.33,
|
||||
"forgotten_per_second": 25.96,
|
||||
"waited_ticks_per_second": 76.05,
|
||||
"chunks_total": 7991,
|
||||
"chunks_per_bot": {
|
||||
"explore": 799.1
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 797.7
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 11028,
|
||||
"moved_too_quickly": 17,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 36.87,
|
||||
"p50": 33.92,
|
||||
"p95": 63.19,
|
||||
"p99": 84.17
|
||||
},
|
||||
"mspt_worst_p99": 124.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1236.2,
|
||||
"max": 1438.4
|
||||
},
|
||||
"cpu_percent": 345.03,
|
||||
"chunks_per_second": 91.07,
|
||||
"unique_per_second": 88.7,
|
||||
"forgotten_per_second": 92.51,
|
||||
"waited_ticks_per_second": 443.88,
|
||||
"chunks_total": 30035,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1201.4
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1176.84
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.55,
|
||||
"mspt": {
|
||||
"average": 24.31,
|
||||
"p50": 21.76,
|
||||
"p95": 36.55,
|
||||
"p99": 155.88
|
||||
},
|
||||
"mspt_worst_p99": 1535.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4047.76,
|
||||
"max": 4459.2
|
||||
},
|
||||
"cpu_percent": 305.24,
|
||||
"chunks_per_second": 37.94,
|
||||
"unique_per_second": 37.82,
|
||||
"forgotten_per_second": 38.87,
|
||||
"waited_ticks_per_second": 261.79,
|
||||
"chunks_total": 16604,
|
||||
"chunks_per_bot": {
|
||||
"explore": 664.16
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 660.92
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 33246,
|
||||
"moved_too_quickly": 20,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.21,
|
||||
"mspt": {
|
||||
"average": 60.06,
|
||||
"p50": 57.31,
|
||||
"p95": 88.38,
|
||||
"p99": 112.89
|
||||
},
|
||||
"mspt_worst_p99": 172.4,
|
||||
"tps": 16.65,
|
||||
"rss_mb": {
|
||||
"mean": 1192.47,
|
||||
"max": 1372.5
|
||||
},
|
||||
"cpu_percent": 362.42,
|
||||
"chunks_per_second": 157.28,
|
||||
"unique_per_second": 154.2,
|
||||
"forgotten_per_second": 156.07,
|
||||
"waited_ticks_per_second": 890.09,
|
||||
"chunks_total": 54551,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1091.02
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1078.5
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.64,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.21,
|
||||
"mspt": {
|
||||
"average": 124.25,
|
||||
"p50": 120.1,
|
||||
"p95": 168.33,
|
||||
"p99": 204.64
|
||||
},
|
||||
"mspt_worst_p99": 262.2,
|
||||
"tps": 8.05,
|
||||
"rss_mb": {
|
||||
"mean": 1283.26,
|
||||
"max": 1418.6
|
||||
},
|
||||
"cpu_percent": 334.14,
|
||||
"chunks_per_second": 185.47,
|
||||
"unique_per_second": 184.54,
|
||||
"forgotten_per_second": 179.79,
|
||||
"waited_ticks_per_second": 1864.45,
|
||||
"chunks_total": 78556,
|
||||
"chunks_per_bot": {
|
||||
"explore": 785.56
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 784.88
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.44,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T18:12:19+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "7bbbf6e97fb53683eae0a01ce2c134e53cb649a7",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50",
|
||||
"behaviour": "explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 180,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0.8,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/explore-equal-rate.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
|
||||
"connect": "<server address>",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.42,
|
||||
"mspt": {
|
||||
"average": 3.16,
|
||||
"p50": 2.98,
|
||||
"p95": 5.78,
|
||||
"p99": 12.17
|
||||
},
|
||||
"mspt_worst_p99": 38.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 759.26,
|
||||
"max": 769.5
|
||||
},
|
||||
"cpu_percent": 21.74,
|
||||
"chunks_per_second": 1.64,
|
||||
"unique_per_second": 1.27,
|
||||
"forgotten_per_second": 1.54,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 841,
|
||||
"chunks_per_bot": {
|
||||
"explore": 841.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 781.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 17.17,
|
||||
"mspt": {
|
||||
"average": 16.82,
|
||||
"p50": 16.67,
|
||||
"p95": 27.3,
|
||||
"p99": 52.16
|
||||
},
|
||||
"mspt_worst_p99": 453.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3373.16,
|
||||
"max": 3405.9
|
||||
},
|
||||
"cpu_percent": 69.22,
|
||||
"chunks_per_second": 1.66,
|
||||
"unique_per_second": 1.35,
|
||||
"forgotten_per_second": 1.54,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 838,
|
||||
"chunks_per_bot": {
|
||||
"explore": 838.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 779.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.82,
|
||||
"mspt": {
|
||||
"average": 9.83,
|
||||
"p50": 8.65,
|
||||
"p95": 19.01,
|
||||
"p99": 32.9
|
||||
},
|
||||
"mspt_worst_p99": 64.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 861.49,
|
||||
"max": 922.2
|
||||
},
|
||||
"cpu_percent": 77.43,
|
||||
"chunks_per_second": 13.71,
|
||||
"unique_per_second": 12.31,
|
||||
"forgotten_per_second": 13.59,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 7831,
|
||||
"chunks_per_bot": {
|
||||
"explore": 783.1
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 757.1
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 1051,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.06,
|
||||
"mspt": {
|
||||
"average": 24.38,
|
||||
"p50": 21.55,
|
||||
"p95": 39.58,
|
||||
"p99": 66.64
|
||||
},
|
||||
"mspt_worst_p99": 107.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3569.41,
|
||||
"max": 3685.1
|
||||
},
|
||||
"cpu_percent": 132.09,
|
||||
"chunks_per_second": 13.54,
|
||||
"unique_per_second": 12.32,
|
||||
"forgotten_per_second": 13.62,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 7753,
|
||||
"chunks_per_bot": {
|
||||
"explore": 775.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 749.2
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 1027,
|
||||
"moved_too_quickly": 6,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 16.87,
|
||||
"p50": 15.4,
|
||||
"p95": 30.88,
|
||||
"p99": 46.73
|
||||
},
|
||||
"mspt_worst_p99": 84.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 887.11,
|
||||
"max": 945.5
|
||||
},
|
||||
"cpu_percent": 111.2,
|
||||
"chunks_per_second": 33.51,
|
||||
"unique_per_second": 29.92,
|
||||
"forgotten_per_second": 33.33,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 19718,
|
||||
"chunks_per_bot": {
|
||||
"explore": 788.72
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 759.08
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3770,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.94,
|
||||
"mspt": {
|
||||
"average": 30.29,
|
||||
"p50": 28.25,
|
||||
"p95": 43.47,
|
||||
"p99": 68.56
|
||||
},
|
||||
"mspt_worst_p99": 165.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3608.97,
|
||||
"max": 3732.0
|
||||
},
|
||||
"cpu_percent": 176.28,
|
||||
"chunks_per_second": 32.98,
|
||||
"unique_per_second": 29.29,
|
||||
"forgotten_per_second": 32.84,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 19442,
|
||||
"chunks_per_bot": {
|
||||
"explore": 777.68
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 748.24
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 5139,
|
||||
"moved_too_quickly": 11,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.41,
|
||||
"mspt": {
|
||||
"average": 23.57,
|
||||
"p50": 20.43,
|
||||
"p95": 47.53,
|
||||
"p99": 67.23
|
||||
},
|
||||
"mspt_worst_p99": 106.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 924.41,
|
||||
"max": 984.3
|
||||
},
|
||||
"cpu_percent": 127.14,
|
||||
"chunks_per_second": 66.56,
|
||||
"unique_per_second": 59.42,
|
||||
"forgotten_per_second": 66.43,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 39950,
|
||||
"chunks_per_bot": {
|
||||
"explore": 799.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 767.56
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 7434,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.04,
|
||||
"mspt": {
|
||||
"average": 42.44,
|
||||
"p50": 40.71,
|
||||
"p95": 54.84,
|
||||
"p99": 94.23
|
||||
},
|
||||
"mspt_worst_p99": 212.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3667.85,
|
||||
"max": 3780.8
|
||||
},
|
||||
"cpu_percent": 200.85,
|
||||
"chunks_per_second": 65.17,
|
||||
"unique_per_second": 58.11,
|
||||
"forgotten_per_second": 65.22,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 39772,
|
||||
"chunks_per_bot": {
|
||||
"explore": 795.44
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 764.02
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 10604,
|
||||
"moved_too_quickly": 38,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
2026-10-17T15:51:54Z thinkpad: 1.30 2.47 3.85 | macmini (bots): { 67.44 76.34 77.44 }
|
||||
2026-10-17T15:52:25Z thinkpad: 2.07 2.54 3.83 | macmini (bots): { 64.78 74.76 76.82 }
|
||||
2026-10-17T15:52:55Z thinkpad: 2.08 2.51 3.78 | macmini (bots): { 62.50 73.29 76.22 }
|
||||
2026-10-17T15:53:25Z thinkpad: 1.46 2.31 3.67 | macmini (bots): { 63.70 72.67 75.90 }
|
||||
2026-10-17T15:53:55Z thinkpad: 1.12 2.15 3.58 | macmini (bots): { 69.06 73.27 76.01 }
|
||||
2026-10-17T15:54:26Z thinkpad: 1.34 2.09 3.51 | macmini (bots): { 72.55 73.63 76.04 }
|
||||
2026-10-17T15:54:56Z thinkpad: 1.11 1.97 3.42 | macmini (bots): { 77.53 74.77 76.37 }
|
||||
2026-10-17T15:55:26Z thinkpad: 1.02 1.87 3.34 | macmini (bots): { 77.61 74.90 76.36 }
|
||||
2026-10-17T15:55:56Z thinkpad: 1.09 1.81 3.27 | macmini (bots): { 71.56 73.69 75.86 }
|
||||
2026-10-17T15:56:26Z thinkpad: 2.23 1.99 3.29 | macmini (bots): { 65.21 71.93 75.15 }
|
||||
2026-10-17T15:56:57Z thinkpad: 2.60 2.12 3.29 | macmini (bots): { 58.16 69.40 74.10 }
|
||||
2026-10-17T15:57:27Z thinkpad: 2.57 2.18 3.27 | macmini (bots): { 57.54 68.26 73.52 }
|
||||
2026-10-17T15:57:57Z thinkpad: 2.37 2.17 3.23 | macmini (bots): { 56.15 66.85 72.83 }
|
||||
2026-10-17T15:58:27Z thinkpad: 1.90 2.08 3.17 | macmini (bots): { 59.28 66.57 72.52 }
|
||||
2026-10-17T15:58:58Z thinkpad: 1.71 2.02 3.11 | macmini (bots): { 55.64 64.94 71.72 }
|
||||
2026-10-17T15:59:28Z thinkpad: 1.66 1.97 3.06 | macmini (bots): { 66.06 66.49 72.04 }
|
||||
2026-10-17T15:59:58Z thinkpad: 1.18 1.83 2.98 | macmini (bots): { 78.15 69.43 72.92 }
|
||||
2026-10-17T16:00:28Z thinkpad: 0.83 1.68 2.89 | macmini (bots): { 70.77 68.57 72.49 }
|
||||
2026-10-17T16:00:58Z thinkpad: 1.27 1.70 2.86 | macmini (bots): { 71.95 68.97 72.49 }
|
||||
2026-10-17T16:01:29Z thinkpad: 1.85 1.79 2.85 | macmini (bots): { 83.43 72.22 73.56 }
|
||||
2026-10-17T16:01:59Z thinkpad: 2.51 1.95 2.86 | macmini (bots): { 70.49 70.18 72.77 }
|
||||
2026-10-17T16:02:29Z thinkpad: 1.98 1.87 2.81 | macmini (bots): { 64.00 68.64 72.12 }
|
||||
2026-10-17T16:02:59Z thinkpad: 1.77 1.84 2.77 | macmini (bots): { 59.65 67.09 71.44 }
|
||||
2026-10-17T16:03:30Z thinkpad: 4.04 2.35 2.91 | macmini (bots): { 62.67 67.15 71.31 }
|
||||
2026-10-17T16:04:00Z thinkpad: 4.94 2.72 3.01 | macmini (bots): { 67.63 67.82 71.40 }
|
||||
2026-10-17T16:04:30Z thinkpad: 4.22 2.78 3.02 | macmini (bots): { 70.41 68.39 71.48 }
|
||||
2026-10-17T16:05:00Z thinkpad: 3.81 2.80 3.02 | macmini (bots): { 66.59 67.83 71.18 }
|
||||
2026-10-17T16:05:31Z thinkpad: 3.20 2.75 3.00 | macmini (bots): { 72.53 69.01 71.48 }
|
||||
2026-10-17T16:06:01Z thinkpad: 4.22 3.04 3.08 | macmini (bots): { 71.30 69.27 71.50 }
|
||||
2026-10-17T16:06:31Z thinkpad: 5.43 3.47 3.23 | macmini (bots): { 68.37 68.63 71.19 }
|
||||
2026-10-17T16:07:01Z thinkpad: 4.35 3.42 3.22 | macmini (bots): { 60.20 66.68 70.39 }
|
||||
2026-10-17T16:07:31Z thinkpad: 3.47 3.31 3.19 | macmini (bots): { 58.03 65.51 69.84 }
|
||||
2026-10-17T16:08:02Z thinkpad: 2.30 3.04 3.11 | macmini (bots): { 59.05 65.13 69.55 }
|
||||
2026-10-17T16:08:32Z thinkpad: 1.88 2.86 3.04 | macmini (bots): { 51.77 62.75 68.53 }
|
||||
2026-10-17T16:09:02Z thinkpad: 1.38 2.65 2.97 | macmini (bots): { 49.05 61.01 67.70 }
|
||||
2026-10-17T16:09:32Z thinkpad: 3.10 2.95 3.06 | macmini (bots): { 48.91 59.82 67.03 }
|
||||
2026-10-17T16:10:02Z thinkpad: 2.91 2.91 3.04 | macmini (bots): { 47.56 58.44 66.25 }
|
||||
2026-10-17T16:10:33Z thinkpad: 2.58 2.83 3.01 | macmini (bots): { 49.23 57.69 65.70 }
|
||||
2026-10-17T16:11:03Z thinkpad: 8.38 4.15 3.44 | macmini (bots): { 44.16 55.72 64.71 }
|
||||
2026-10-17T16:11:33Z thinkpad: 9.94 4.92 3.72 | macmini (bots): { 44.50 54.71 64.03 }
|
||||
2026-10-17T16:12:03Z thinkpad: 10.08 5.42 3.92 | macmini (bots): { 42.15 53.21 63.17 }
|
||||
2026-10-17T16:12:33Z thinkpad: 6.79 5.06 3.85 | macmini (bots): { 36.13 50.73 61.92 }
|
||||
2026-10-17T16:13:04Z thinkpad: 4.61 4.72 3.77 | macmini (bots): { 41.28 50.64 61.51 }
|
||||
2026-10-17T16:13:34Z thinkpad: 3.81 4.50 3.73 | macmini (bots): { 42.92 50.15 60.95 }
|
||||
2026-10-17T16:14:04Z thinkpad: 3.39 4.35 3.71 | macmini (bots): { 41.56 49.16 60.22 }
|
||||
2026-10-17T16:14:35Z thinkpad: 7.53 5.17 4.00 | macmini (bots): { 47.48 49.82 60.07 }
|
||||
2026-10-17T16:15:05Z thinkpad: 9.73 6.01 4.32 | macmini (bots): { 51.43 50.41 59.92 }
|
||||
2026-10-17T16:15:35Z thinkpad: 8.07 5.94 4.35 | macmini (bots): { 69.31 54.81 61.18 }
|
||||
2026-10-17T16:16:05Z thinkpad: 9.27 6.45 4.57 | macmini (bots): { 71.85 56.92 61.73 }
|
||||
2026-10-17T16:16:35Z thinkpad: 8.69 6.59 4.68 | macmini (bots): { 64.77 56.72 61.50 }
|
||||
2026-10-17T16:17:06Z thinkpad: 6.11 6.17 4.60 | macmini (bots): { 57.92 55.86 61.02 }
|
||||
2026-10-17T16:17:36Z thinkpad: 6.79 6.33 4.70 | macmini (bots): { 51.44 54.51 60.36 }
|
||||
2026-10-17T16:18:06Z thinkpad: 5.16 5.99 4.64 | macmini (bots): { 51.33 54.23 60.05 }
|
||||
2026-10-17T16:18:36Z thinkpad: 5.30 5.93 4.66 | macmini (bots): { 55.22 54.87 60.08 }
|
||||
2026-10-17T16:19:07Z thinkpad: 4.78 5.72 4.63 | macmini (bots): { 51.62 53.94 59.56 }
|
||||
2026-10-17T16:19:37Z thinkpad: 5.25 5.78 4.69 | macmini (bots): { 48.77 53.04 59.04 }
|
||||
2026-10-17T16:20:07Z thinkpad: 4.63 5.60 4.67 | macmini (bots): { 54.59 53.92 59.14 }
|
||||
2026-10-17T16:20:37Z thinkpad: 5.46 5.67 4.72 | macmini (bots): { 59.07 55.03 59.36 }
|
||||
2026-10-17T16:21:07Z thinkpad: 8.07 6.19 4.91 | macmini (bots): { 52.53 53.83 58.77 }
|
||||
2026-10-17T16:21:38Z thinkpad: 15.01 8.05 5.57 | macmini (bots): { 48.85 52.78 58.19 }
|
||||
2026-10-17T16:22:08Z thinkpad: 19.08 9.69 6.20 | macmini (bots): { 54.73 53.61 58.29 }
|
||||
2026-10-17T16:22:39Z thinkpad: 27.72 12.63 7.29 | macmini (bots): { 54.23 53.61 58.13 }
|
||||
2026-10-17T16:23:09Z thinkpad: 23.10 12.88 7.53 | macmini (bots): { 51.63 53.06 57.77 }
|
||||
2026-10-17T16:23:39Z thinkpad: 20.51 13.53 7.96 | macmini (bots): { 45.29 51.38 57.00 }
|
||||
2026-10-17T16:24:10Z thinkpad: 12.70 12.30 7.72 | macmini (bots): { 45.61 50.91 56.63 }
|
||||
2026-10-17T16:24:40Z thinkpad: 8.67 11.35 7.55 | macmini (bots): { 45.18 50.35 56.23 }
|
||||
2026-10-17T16:25:10Z thinkpad: 6.35 10.53 7.40 | macmini (bots): { 41.10 48.88 55.50 }
|
||||
2026-10-17T16:25:40Z thinkpad: 6.26 10.16 7.38 | macmini (bots): { 49.86 50.24 55.76 }
|
||||
2026-10-17T16:26:11Z thinkpad: 7.71 10.11 7.45 | macmini (bots): { 53.93 51.11 55.88 }
|
||||
2026-10-17T16:26:41Z thinkpad: 9.50 10.32 7.61 | macmini (bots): { 47.61 49.86 55.26 }
|
||||
2026-10-17T16:27:11Z thinkpad: 10.26 10.39 7.72 | macmini (bots): { 46.43 49.36 54.89 }
|
||||
2026-10-17T16:27:42Z thinkpad: 10.61 10.48 7.83 | macmini (bots): { 48.88 49.66 54.80 }
|
||||
2026-10-17T16:28:12Z thinkpad: 10.39 10.45 7.91 | macmini (bots): { 58.42 51.74 55.37 }
|
||||
2026-10-17T16:28:42Z thinkpad: 10.36 10.44 7.99 | macmini (bots): { 59.59 52.67 55.58 }
|
||||
2026-10-17T16:29:13Z thinkpad: 9.63 10.25 8.00 | macmini (bots): { 55.30 52.38 55.38 }
|
||||
2026-10-17T16:29:43Z thinkpad: 10.33 10.37 8.12 | macmini (bots): { 57.79 53.13 55.52 }
|
||||
2026-10-17T16:30:13Z thinkpad: 9.68 10.18 8.12 | macmini (bots): { 57.83 53.60 55.61 }
|
||||
2026-10-17T16:30:43Z thinkpad: 9.72 10.17 8.19 | macmini (bots): { 78.94 59.18 57.56 }
|
||||
2026-10-17T16:31:14Z thinkpad: 6.60 9.36 7.98 | macmini (bots): { 87.29 62.98 58.99 }
|
||||
2026-10-17T16:31:44Z thinkpad: 6.77 9.08 7.93 | macmini (bots): { 81.82 64.16 59.57 }
|
||||
2026-10-17T16:32:14Z thinkpad: 13.15 10.41 8.41 | macmini (bots): { 81.20 65.92 60.37 }
|
||||
2026-10-17T16:32:45Z thinkpad: 27.80 14.12 9.71 | macmini (bots): { 75.41 66.22 60.69 }
|
||||
2026-10-17T16:33:15Z thinkpad: 44.59 19.25 11.54 | macmini (bots): { 66.92 65.08 60.47 }
|
||||
2026-10-17T16:33:46Z thinkpad: 44.87 21.89 12.67 | macmini (bots): { 61.47 63.94 60.22 }
|
||||
2026-10-17T16:34:16Z thinkpad: 38.98 22.67 13.23 | macmini (bots): { 62.13 63.78 60.28 }
|
||||
2026-10-17T16:34:47Z thinkpad: 47.64 26.08 14.65 | macmini (bots): { 81.23 68.06 61.94 }
|
||||
2026-10-17T16:35:17Z thinkpad: 47.83 28.02 15.65 | macmini (bots): { 116.14 78.43 65.98 }
|
||||
2026-10-17T16:35:48Z thinkpad: 40.43 28.18 16.10 | macmini (bots): { 125.61 84.41 68.58 }
|
||||
2026-10-17T16:36:19Z thinkpad: 41.34 29.71 17.00 | macmini (bots): { 143.61 92.86 72.20 }
|
||||
2026-10-17T16:36:49Z thinkpad: 34.40 29.19 17.29 | macmini (bots): { 156.05 100.88 75.83 }
|
||||
2026-10-17T16:37:20Z thinkpad: 25.34 27.65 17.17 | macmini (bots): { 151.90 105.26 78.29 }
|
||||
2026-10-17T16:37:50Z thinkpad: 18.36 25.71 16.85 | macmini (bots): { 141.10 107.25 79.96 }
|
||||
2026-10-17T16:38:20Z thinkpad: 16.19 24.47 16.72 | macmini (bots): { 131.21 107.92 81.13 }
|
||||
2026-10-17T16:38:51Z thinkpad: 13.36 23.00 16.48 | macmini (bots): { 122.08 107.78 82.00 }
|
||||
2026-10-17T16:39:21Z thinkpad: 11.27 21.57 16.21 | macmini (bots): { 107.07 105.50 82.07 }
|
||||
2026-10-17T16:39:51Z thinkpad: 11.19 20.56 16.04 | macmini (bots): { 96.38 103.12 82.02 }
|
||||
2026-10-17T16:40:22Z thinkpad: 10.06 19.37 15.79 | macmini (bots): { 89.06 100.79 81.91 }
|
||||
2026-10-17T16:40:52Z thinkpad: 7.38 17.86 15.40 | macmini (bots): { 79.35 97.41 81.34 }
|
||||
2026-10-17T16:41:23Z thinkpad: 6.18 16.57 15.05 | macmini (bots): { 81.70 96.20 81.46 }
|
||||
2026-10-17T16:41:53Z thinkpad: 5.56 15.44 14.72 | macmini (bots): { 86.51 95.88 81.84 }
|
||||
2026-10-17T16:42:23Z thinkpad: 4.90 14.33 14.37 | macmini (bots): { 99.20 97.69 83.05 }
|
||||
2026-10-17T16:42:53Z thinkpad: 5.01 13.47 14.08 | macmini (bots): { 116.29 101.87 85.07 }
|
||||
2026-10-17T16:43:24Z thinkpad: 4.34 12.50 13.74 | macmini (bots): { 131.32 106.77 87.42 }
|
||||
2026-10-17T16:43:54Z thinkpad: 4.08 11.65 13.42 | macmini (bots): { 155.44 114.77 90.98 }
|
||||
2026-10-17T16:44:24Z thinkpad: 4.00 10.90 13.11 | macmini (bots): { 167.86 121.62 94.28 }
|
||||
2026-10-17T16:44:54Z thinkpad: 3.98 10.13 12.77 | macmini (bots): { 160.36 124.31 96.20 }
|
||||
2026-10-17T16:45:25Z thinkpad: 4.20 9.59 12.50 | macmini (bots): { 163.23 128.59 98.74 }
|
||||
2026-10-17T16:45:55Z thinkpad: 4.54 9.15 12.27 | macmini (bots): { 169.93 133.73 101.65 }
|
||||
2026-10-17T16:46:25Z thinkpad: 4.44 8.69 12.01 | macmini (bots): { 162.40 135.22 103.29 }
|
||||
2026-10-17T16:46:55Z thinkpad: 4.06 8.20 11.74 | macmini (bots): { 164.16 138.31 105.52 }
|
||||
2026-10-17T16:47:26Z thinkpad: 3.96 7.78 11.49 | macmini (bots): { 157.92 139.31 107.02 }
|
||||
2026-10-17T16:47:56Z thinkpad: 3.71 7.35 11.23 | macmini (bots): { 155.72 140.55 108.59 }
|
||||
2026-10-17T16:48:26Z thinkpad: 4.93 7.32 11.09 | macmini (bots): { 153.39 141.52 110.05 }
|
||||
2026-10-17T16:48:56Z thinkpad: 4.61 7.03 10.88 | macmini (bots): { 137.54 138.87 110.18 }
|
||||
2026-10-17T16:49:27Z thinkpad: 4.01 6.65 10.63 | macmini (bots): { 136.53 138.36 110.98 }
|
||||
2026-10-17T16:49:57Z thinkpad: 3.19 6.19 10.35 | macmini (bots): { 141.12 139.18 112.22 }
|
||||
2026-10-17T16:50:27Z thinkpad: 5.31 6.43 10.30 | macmini (bots): { 122.76 135.27 111.75 }
|
||||
2026-10-17T16:50:57Z thinkpad: 5.95 6.48 10.19 | macmini (bots): { 114.26 132.02 111.39 }
|
||||
2026-10-17T16:51:28Z thinkpad: 5.76 6.41 10.05 | macmini (bots): { 116.04 130.87 111.69 }
|
||||
2026-10-17T16:51:58Z thinkpad: 5.11 6.18 9.86 | macmini (bots): { 115.06 129.17 111.74 }
|
||||
2026-10-17T16:52:28Z thinkpad: 5.09 6.07 9.70 | macmini (bots): { 109.91 126.59 111.41 }
|
||||
2026-10-17T16:52:58Z thinkpad: 4.74 5.90 9.53 | macmini (bots): { 101.90 123.11 110.67 }
|
||||
2026-10-17T16:53:29Z thinkpad: 4.89 5.84 9.39 | macmini (bots): { 83.29 116.38 108.74 }
|
||||
2026-10-17T16:53:59Z thinkpad: 5.10 5.82 9.27 | macmini (bots): { 119.16 122.10 111.09 }
|
||||
2026-10-17T16:54:29Z thinkpad: 4.57 5.67 9.10 | macmini (bots): { 114.87 120.76 110.98 }
|
||||
2026-10-17T16:55:00Z thinkpad: 4.74 5.58 8.96 | macmini (bots): { 129.84 123.74 112.39 }
|
||||
2026-10-17T16:55:30Z thinkpad: 6.40 5.90 8.96 | macmini (bots): { 100.10 117.29 110.46 }
|
||||
2026-10-17T16:56:00Z thinkpad: 6.96 6.08 8.92 | macmini (bots): { 80.18 110.75 108.32 }
|
||||
2026-10-17T16:56:30Z thinkpad: 6.07 5.95 8.79 | macmini (bots): { 74.41 106.39 106.82 }
|
||||
2026-10-17T16:57:01Z thinkpad: 6.63 6.13 8.75 | macmini (bots): { 81.71 104.99 106.29 }
|
||||
2026-10-17T16:57:31Z thinkpad: 6.39 6.14 8.68 | macmini (bots): { 101.10 107.55 107.18 }
|
||||
2026-10-17T16:58:01Z thinkpad: 6.32 6.16 8.60 | macmini (bots): { 94.45 105.66 106.52 }
|
||||
2026-10-17T16:58:31Z thinkpad: 5.19 5.90 8.44 | macmini (bots): { 78.76 100.83 104.75 }
|
||||
2026-10-17T16:59:02Z thinkpad: 5.24 5.83 8.33 | macmini (bots): { 78.19 98.42 103.73 }
|
||||
2026-10-17T16:59:32Z thinkpad: 3.95 5.48 8.13 | macmini (bots): { 77.57 96.42 102.82 }
|
||||
2026-10-17T17:00:02Z thinkpad: 3.12 5.13 7.93 | macmini (bots): { 85.23 96.29 102.54 }
|
||||
2026-10-17T17:00:32Z thinkpad: 3.17 4.93 7.77 | macmini (bots): { 99.84 99.07 103.35 }
|
||||
2026-10-17T17:01:02Z thinkpad: 6.09 5.50 7.88 | macmini (bots): { 80.56 94.49 101.54 }
|
||||
2026-10-17T17:01:33Z thinkpad: 9.23 6.29 8.06 | macmini (bots): { 79.70 93.03 100.77 }
|
||||
2026-10-17T17:02:03Z thinkpad: 9.09 6.54 8.09 | macmini (bots): { 76.63 90.94 99.74 }
|
||||
2026-10-17T17:02:33Z thinkpad: 7.86 6.52 8.04 | macmini (bots): { 93.08 93.50 100.36 }
|
||||
2026-10-17T17:03:03Z thinkpad: 7.81 6.67 8.04 | macmini (bots): { 83.14 91.09 99.25 }
|
||||
2026-10-17T17:03:34Z thinkpad: 6.69 6.51 7.94 | macmini (bots): { 79.37 89.64 98.45 }
|
||||
2026-10-17T17:04:04Z thinkpad: 5.63 6.27 7.81 | macmini (bots): { 73.68 87.23 97.27 }
|
||||
2026-10-17T17:04:34Z thinkpad: 5.75 6.21 7.75 | macmini (bots): { 75.89 86.23 96.54 }
|
||||
2026-10-17T17:05:04Z thinkpad: 5.05 6.01 7.63 | macmini (bots): { 79.29 86.13 96.10 }
|
||||
2026-10-17T17:05:35Z thinkpad: 3.88 5.62 7.44 | macmini (bots): { 76.07 84.79 95.28 }
|
||||
2026-10-17T17:06:05Z thinkpad: 6.47 6.07 7.53 | macmini (bots): { 70.59 82.62 94.13 }
|
||||
2026-10-17T17:06:35Z thinkpad: 8.20 6.53 7.63 | macmini (bots): { 69.09 81.18 93.21 }
|
||||
2026-10-17T17:07:05Z thinkpad: 7.59 6.54 7.60 | macmini (bots): { 77.55 82.11 93.13 }
|
||||
2026-10-17T17:07:36Z thinkpad: 6.93 6.49 7.55 | macmini (bots): { 84.23 83.41 93.22 }
|
||||
2026-10-17T17:08:06Z thinkpad: 7.21 6.61 7.56 | macmini (bots): { 84.31 83.68 92.99 }
|
||||
2026-10-17T17:08:36Z thinkpad: 6.73 6.57 7.51 | macmini (bots): { 81.04 82.99 92.42 }
|
||||
2026-10-17T17:09:06Z thinkpad: 6.12 6.43 7.44 | macmini (bots): { 86.55 83.77 92.35 }
|
||||
2026-10-17T17:09:36Z thinkpad: 6.77 6.58 7.46 | macmini (bots): { 95.19 86.31 92.99 }
|
||||
2026-10-17T17:10:07Z thinkpad: 7.13 6.68 7.46 | macmini (bots): { 111.30 91.11 94.50 }
|
||||
2026-10-17T17:10:37Z thinkpad: 5.09 6.23 7.29 | macmini (bots): { 118.51 95.17 95.87 }
|
||||
2026-10-17T17:11:07Z thinkpad: 4.00 5.86 7.13 | macmini (bots): { 121.12 98.01 96.88 }
|
||||
2026-10-17T17:11:38Z thinkpad: 3.50 5.57 6.99 | macmini (bots): { 109.57 97.67 96.81 }
|
||||
2026-10-17T17:12:08Z thinkpad: 2.92 5.22 6.83 | macmini (bots): { 108.43 98.44 97.11 }
|
||||
2026-10-17T17:12:38Z thinkpad: 3.42 5.12 6.74 | macmini (bots): { 101.61 97.86 96.95 }
|
||||
2026-10-17T17:13:09Z thinkpad: 5.64 5.51 6.82 | macmini (bots): { 101.66 98.17 97.09 }
|
||||
2026-10-17T17:13:39Z thinkpad: 7.06 5.89 6.91 | macmini (bots): { 88.43 95.36 96.11 }
|
||||
2026-10-17T17:14:09Z thinkpad: 7.61 6.13 6.96 | macmini (bots): { 79.03 92.54 95.07 }
|
||||
2026-10-17T17:14:39Z thinkpad: 9.06 6.68 7.12 | macmini (bots): { 92.55 94.57 95.72 }
|
||||
2026-10-17T17:15:10Z thinkpad: 9.81 7.06 7.22 | macmini (bots): { 93.71 94.49 95.64 }
|
||||
2026-10-17T17:15:40Z thinkpad: 9.98 7.41 7.34 | macmini (bots): { 92.26 93.86 95.35 }
|
||||
2026-10-17T17:16:10Z thinkpad: 7.60 7.08 7.23 | macmini (bots): { 88.61 92.91 94.96 }
|
||||
2026-10-17T17:16:40Z thinkpad: 6.81 6.93 7.17 | macmini (bots): { 96.96 94.47 95.45 }
|
||||
2026-10-17T17:17:11Z thinkpad: 6.31 6.81 7.12 | macmini (bots): { 102.92 96.14 96.02 }
|
||||
2026-10-17T17:17:41Z thinkpad: 4.81 6.38 6.97 | macmini (bots): { 91.20 94.16 95.32 }
|
||||
2026-10-17T17:18:11Z thinkpad: 7.36 6.81 7.09 | macmini (bots): { 93.60 94.31 95.32 }
|
||||
2026-10-17T17:18:41Z thinkpad: 9.42 7.38 7.27 | macmini (bots): { 90.86 93.57 95.02 }
|
||||
2026-10-17T17:19:11Z thinkpad: 9.95 7.72 7.39 | macmini (bots): { 88.94 92.94 94.75 }
|
||||
2026-10-17T17:19:42Z thinkpad: 9.51 7.85 7.45 | macmini (bots): { 94.97 93.93 95.03 }
|
||||
2026-10-17T17:20:12Z thinkpad: 8.97 7.90 7.48 | macmini (bots): { 80.46 90.39 93.71 }
|
||||
2026-10-17T17:20:42Z thinkpad: 7.56 7.65 7.41 | macmini (bots): { 74.38 87.95 92.71 }
|
||||
2026-10-17T17:21:12Z thinkpad: 6.39 7.36 7.32 | macmini (bots): { 77.48 87.26 92.28 }
|
||||
2026-10-17T17:21:43Z thinkpad: 5.21 6.98 7.19 | macmini (bots): { 82.05 87.48 92.19 }
|
||||
2026-10-17T17:22:13Z thinkpad: 3.87 6.49 7.02 | macmini (bots): { 88.15 88.45 92.38 }
|
||||
2026-10-17T17:22:43Z thinkpad: 3.86 6.22 6.91 | macmini (bots): { 100.23 91.12 93.19 }
|
||||
2026-10-17T17:23:13Z thinkpad: 8.32 7.05 7.16 | macmini (bots): { 84.55 88.39 92.15 }
|
||||
2026-10-17T17:23:43Z thinkpad: 13.84 8.37 7.59 | macmini (bots): { 73.74 85.38 90.92 }
|
||||
2026-10-17T17:24:14Z thinkpad: 21.02 10.71 8.40 | macmini (bots): { 71.36 83.72 90.13 }
|
||||
2026-10-17T17:24:44Z thinkpad: 20.73 11.82 8.85 | macmini (bots): { 82.27 85.04 90.38 }
|
||||
2026-10-17T17:25:14Z thinkpad: 16.80 11.77 8.94 | macmini (bots): { 78.63 83.92 89.78 }
|
||||
2026-10-17T17:25:44Z thinkpad: 14.62 11.68 8.99 | macmini (bots): { 82.33 84.06 89.61 }
|
||||
2026-10-17T17:26:15Z thinkpad: 14.51 11.94 9.17 | macmini (bots): { 74.13 81.89 88.63 }
|
||||
2026-10-17T17:26:45Z thinkpad: 12.48 11.74 9.20 | macmini (bots): { 75.65 81.46 88.24 }
|
||||
2026-10-17T17:27:15Z thinkpad: 15.24 12.56 9.56 | macmini (bots): { 71.35 79.78 87.35 }
|
||||
2026-10-17T17:27:45Z thinkpad: 15.70 12.87 9.76 | macmini (bots): { 68.21 78.17 86.51 }
|
||||
2026-10-17T17:28:16Z thinkpad: 12.80 12.47 9.72 | macmini (bots): { 79.74 80.10 86.92 }
|
||||
2026-10-17T17:28:46Z thinkpad: 9.05 11.58 9.51 | macmini (bots): { 101.01 85.27 88.56 }
|
||||
2026-10-17T17:29:16Z thinkpad: 9.38 11.41 9.53 | macmini (bots): { 93.76 85.18 88.43 }
|
||||
2026-10-17T17:29:46Z thinkpad: 11.08 11.62 9.65 | macmini (bots): { 74.91 81.56 87.01 }
|
||||
2026-10-17T17:30:17Z thinkpad: 10.98 11.53 9.69 | macmini (bots): { 61.10 77.50 85.34 }
|
||||
2026-10-17T17:30:47Z thinkpad: 9.83 11.21 9.64 | macmini (bots): { 51.19 73.56 83.64 }
|
||||
2026-10-17T17:31:18Z thinkpad: 9.65 11.03 9.63 | macmini (bots): { 49.64 71.03 82.37 }
|
||||
2026-10-17T17:31:48Z thinkpad: 7.22 10.33 9.44 | macmini (bots): { 53.62 69.90 81.56 }
|
||||
2026-10-17T17:32:18Z thinkpad: 5.79 9.68 9.25 | macmini (bots): { 61.07 70.36 81.34 }
|
||||
2026-10-17T17:32:48Z thinkpad: 3.70 8.80 8.97 | macmini (bots): { 60.39 69.24 80.55 }
|
||||
2026-10-17T17:33:19Z thinkpad: 2.72 8.08 8.73 | macmini (bots): { 57.06 67.51 79.53 }
|
||||
2026-10-17T17:33:49Z thinkpad: 1.97 7.37 8.47 | macmini (bots): { 65.72 68.41 79.42 }
|
||||
2026-10-17T17:34:19Z thinkpad: 1.25 6.68 8.20 | macmini (bots): { 62.60 67.40 78.67 }
|
||||
2026-10-17T17:34:49Z thinkpad: 0.94 6.09 7.96 | macmini (bots): { 71.83 69.24 78.95 }
|
||||
2026-10-17T17:35:20Z thinkpad: 0.72 5.54 7.71 | macmini (bots): { 66.16 68.07 78.19 }
|
||||
2026-10-17T17:35:50Z thinkpad: 0.44 5.01 7.47 | macmini (bots): { 70.96 68.91 78.13 }
|
||||
2026-10-17T17:36:20Z thinkpad: 1.70 4.79 7.30 | macmini (bots): { 79.74 71.45 78.74 }
|
||||
2026-10-17T17:36:50Z thinkpad: 3.03 4.80 7.22 | macmini (bots): { 79.71 72.75 78.95 }
|
||||
2026-10-17T17:37:21Z thinkpad: 3.07 4.67 7.10 | macmini (bots): { 85.13 74.60 79.40 }
|
||||
2026-10-17T17:37:51Z thinkpad: 2.32 4.33 6.91 | macmini (bots): { 84.85 75.70 79.64 }
|
||||
2026-10-17T17:38:21Z thinkpad: 1.83 4.03 6.73 | macmini (bots): { 86.47 76.79 79.88 }
|
||||
2026-10-17T17:38:51Z thinkpad: 1.11 3.64 6.51 | macmini (bots): { 106.96 82.74 81.94 }
|
||||
2026-10-17T17:39:22Z thinkpad: 1.03 3.37 6.33 | macmini (bots): { 114.52 87.01 83.52 }
|
||||
2026-10-17T17:39:52Z thinkpad: 1.11 3.16 6.16 | macmini (bots): { 103.99 87.05 83.65 }
|
||||
2026-10-17T17:40:22Z thinkpad: 1.31 3.02 6.02 | macmini (bots): { 100.27 87.59 83.95 }
|
||||
2026-10-17T17:40:52Z thinkpad: 1.36 2.87 5.87 | macmini (bots): { 98.17 88.44 84.40 }
|
||||
2026-10-17T17:41:22Z thinkpad: 2.01 2.88 5.78 | macmini (bots): { 96.27 88.67 84.60 }
|
||||
2026-10-17T17:41:53Z thinkpad: 1.96 2.77 5.65 | macmini (bots): { 96.31 89.44 85.03 }
|
||||
2026-10-17T17:42:23Z thinkpad: 3.06 2.97 5.62 | macmini (bots): { 80.49 86.30 84.04 }
|
||||
2026-10-17T17:42:53Z thinkpad: 2.38 2.81 5.49 | macmini (bots): { 66.40 82.24 82.64 }
|
||||
2026-10-17T17:43:24Z thinkpad: 2.44 2.81 5.40 | macmini (bots): { 56.70 78.37 81.22 }
|
||||
2026-10-17T17:43:54Z thinkpad: 1.93 2.65 5.26 | macmini (bots): { 61.56 77.39 80.77 }
|
||||
2026-10-17T17:44:24Z thinkpad: 1.96 2.59 5.16 | macmini (bots): { 58.25 75.14 79.83 }
|
||||
2026-10-17T17:44:54Z thinkpad: 2.25 2.60 5.08 | macmini (bots): { 56.32 73.21 78.98 }
|
||||
2026-10-17T17:45:24Z thinkpad: 1.79 2.46 4.95 | macmini (bots): { 62.87 73.14 78.75 }
|
||||
2026-10-17T17:45:55Z thinkpad: 2.78 2.64 4.93 | macmini (bots): { 62.58 72.22 78.23 }
|
||||
2026-10-17T17:46:25Z thinkpad: 4.82 3.14 5.03 | macmini (bots): { 58.89 70.44 77.38 }
|
||||
2026-10-17T17:46:55Z thinkpad: 3.10 2.90 4.88 | macmini (bots): { 61.68 70.08 77.01 }
|
||||
2026-10-17T17:47:25Z thinkpad: 2.27 2.71 4.75 | macmini (bots): { 70.43 71.40 77.25 }
|
||||
2026-10-17T17:47:56Z thinkpad: 1.71 2.53 4.62 | macmini (bots): { 70.51 71.56 77.12 }
|
||||
2026-10-17T17:48:26Z thinkpad: 1.59 2.42 4.52 | macmini (bots): { 68.98 70.98 76.71 }
|
||||
2026-10-17T17:48:56Z thinkpad: 2.09 2.47 4.47 | macmini (bots): { 67.75 70.60 76.38 }
|
||||
2026-10-17T17:49:26Z thinkpad: 2.14 2.44 4.39 | macmini (bots): { 61.53 69.01 75.58 }
|
||||
2026-10-17T17:49:56Z thinkpad: 2.22 2.43 4.33 | macmini (bots): { 58.49 67.45 74.78 }
|
||||
2026-10-17T17:50:27Z thinkpad: 1.98 2.36 4.24 | macmini (bots): { 66.15 68.33 74.84 }
|
||||
2026-10-17T17:50:57Z thinkpad: 2.38 2.42 4.20 | macmini (bots): { 61.65 67.22 74.22 }
|
||||
2026-10-17T17:51:27Z thinkpad: 3.62 2.72 4.24 | macmini (bots): { 54.05 64.88 73.13 }
|
||||
2026-10-17T17:51:57Z thinkpad: 3.25 2.70 4.19 | macmini (bots): { 49.14 62.62 72.02 }
|
||||
2026-10-17T17:52:28Z thinkpad: 2.45 2.55 4.09 | macmini (bots): { 43.72 60.06 70.77 }
|
||||
2026-10-17T17:52:58Z thinkpad: 2.85 2.62 4.06 | macmini (bots): { 42.53 58.22 69.73 }
|
||||
2026-10-17T17:53:28Z thinkpad: 8.44 3.97 4.46 | macmini (bots): { 45.21 57.34 69.01 }
|
||||
2026-10-17T17:53:58Z thinkpad: 10.75 4.91 4.76 | macmini (bots): { 44.86 56.01 68.11 }
|
||||
2026-10-17T17:54:29Z thinkpad: 8.36 4.90 4.76 | macmini (bots): { 51.73 56.60 67.91 }
|
||||
2026-10-17T17:54:59Z thinkpad: 7.41 4.98 4.79 | macmini (bots): { 55.80 57.05 67.67 }
|
||||
2026-10-17T17:55:29Z thinkpad: 5.45 4.73 4.71 | macmini (bots): { 50.40 55.67 66.81 }
|
||||
2026-10-17T17:55:59Z thinkpad: 6.26 4.97 4.79 | macmini (bots): { 57.22 56.88 66.87 }
|
||||
2026-10-17T17:56:30Z thinkpad: 8.22 5.52 4.98 | macmini (bots): { 48.94 54.86 65.78 }
|
||||
2026-10-17T17:57:00Z thinkpad: 6.71 5.42 4.96 | macmini (bots): { 50.54 54.70 65.35 }
|
||||
2026-10-17T17:57:30Z thinkpad: 4.94 5.14 4.89 | macmini (bots): { 58.02 56.25 65.55 }
|
||||
2026-10-17T17:58:01Z thinkpad: 4.54 5.04 4.86 | macmini (bots): { 53.98 55.38 64.90 }
|
||||
2026-10-17T17:58:31Z thinkpad: 4.65 5.04 4.87 | macmini (bots): { 49.96 54.23 64.16 }
|
||||
2026-10-17T17:59:01Z thinkpad: 5.29 5.16 4.92 | macmini (bots): { 49.63 53.68 63.61 }
|
||||
2026-10-17T17:59:31Z thinkpad: 4.23 4.94 4.85 | macmini (bots): { 57.15 55.10 63.78 }
|
||||
2026-10-17T18:00:01Z thinkpad: 3.57 4.72 4.78 | macmini (bots): { 61.70 56.26 63.89 }
|
||||
2026-10-17T18:00:32Z thinkpad: 3.16 4.52 4.71 | macmini (bots): { 62.10 56.86 63.79 }
|
||||
2026-10-17T18:01:02Z thinkpad: 2.81 4.29 4.63 | macmini (bots): { 76.29 60.79 64.98 }
|
||||
2026-10-17T18:01:32Z thinkpad: 7.77 5.26 4.93 | macmini (bots): { 75.36 62.16 65.34 }
|
||||
2026-10-17T18:02:02Z thinkpad: 11.60 6.45 5.34 | macmini (bots): { 70.83 62.28 65.27 }
|
||||
2026-10-17T18:02:33Z thinkpad: 11.54 6.97 5.56 | macmini (bots): { 65.66 61.87 65.01 }
|
||||
2026-10-17T18:03:03Z thinkpad: 10.80 7.33 5.73 | macmini (bots): { 63.09 61.54 64.78 }
|
||||
2026-10-17T18:03:33Z thinkpad: 10.52 7.56 5.85 | macmini (bots): { 60.31 61.12 64.52 }
|
||||
2026-10-17T18:04:04Z thinkpad: 10.27 7.76 5.97 | macmini (bots): { 60.99 61.21 64.44 }
|
||||
2026-10-17T18:04:34Z thinkpad: 8.77 7.69 6.01 | macmini (bots): { 56.72 60.19 63.96 }
|
||||
2026-10-17T18:05:05Z thinkpad: 5.77 7.06 5.85 | macmini (bots): { 56.14 59.70 63.65 }
|
||||
2026-10-17T18:05:35Z thinkpad: 5.16 6.79 5.80 | macmini (bots): { 51.25 58.17 62.95 }
|
||||
2026-10-17T18:06:05Z thinkpad: 4.16 6.36 5.69 | macmini (bots): { 52.03 57.64 62.59 }
|
||||
2026-10-17T18:06:35Z thinkpad: 4.28 6.18 5.65 | macmini (bots): { 64.66 60.00 63.27 }
|
||||
2026-10-17T18:07:06Z thinkpad: 7.47 6.76 5.86 | macmini (bots): { 56.20 58.45 62.59 }
|
||||
2026-10-17T18:07:36Z thinkpad: 6.95 6.74 5.89 | macmini (bots): { 51.92 57.20 62.00 }
|
||||
2026-10-17T18:08:06Z thinkpad: 7.70 6.99 6.00 | macmini (bots): { 51.14 56.65 61.64 }
|
||||
2026-10-17T18:08:36Z thinkpad: 5.83 6.60 5.90 | macmini (bots): { 48.08 55.33 60.98 }
|
||||
2026-10-17T18:09:07Z thinkpad: 4.89 6.30 5.82 | macmini (bots): { 53.80 56.00 61.00 }
|
||||
2026-10-17T18:09:37Z thinkpad: 4.93 6.19 5.80 | macmini (bots): { 60.49 57.24 61.27 }
|
||||
2026-10-17T18:10:07Z thinkpad: 4.61 5.98 5.74 | macmini (bots): { 58.95 57.21 61.12 }
|
||||
2026-10-17T18:10:37Z thinkpad: 4.74 5.91 5.73 | macmini (bots): { 60.84 57.65 61.13 }
|
||||
2026-10-17T18:11:07Z thinkpad: 3.96 5.61 5.63 | macmini (bots): { 64.23 58.82 61.43 }
|
||||
2026-10-17T18:11:38Z thinkpad: 4.16 5.53 5.61 | macmini (bots): { 75.59 62.26 62.60 }
|
||||
2026-10-17T18:12:08Z thinkpad: 2.97 5.11 5.46 | macmini (bots): { 75.25 63.65 63.10 }
|
||||
@@ -1,695 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T17:31:40+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "7bbbf6e97fb53683eae0a01ce2c134e53cb649a7",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50,100",
|
||||
"behaviour": "walk,explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 180,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/walk-explore.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
|
||||
"connect": "<server address>",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.62,
|
||||
"mspt": {
|
||||
"average": 4.01,
|
||||
"p50": 3.54,
|
||||
"p95": 8.19,
|
||||
"p99": 25.59
|
||||
},
|
||||
"mspt_worst_p99": 47.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 782.52,
|
||||
"max": 785.3
|
||||
},
|
||||
"cpu_percent": 58.87,
|
||||
"chunks_per_second": 9.6,
|
||||
"unique_per_second": 0.43,
|
||||
"forgotten_per_second": 9.65,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 2697,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2697.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 693.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.85,
|
||||
"mspt": {
|
||||
"average": 18.22,
|
||||
"p50": 17.99,
|
||||
"p95": 29.34,
|
||||
"p99": 50.06
|
||||
},
|
||||
"mspt_worst_p99": 395.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3429.06,
|
||||
"max": 3446.3
|
||||
},
|
||||
"cpu_percent": 68.81,
|
||||
"chunks_per_second": 10.47,
|
||||
"unique_per_second": 0.47,
|
||||
"forgotten_per_second": 10.53,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 2755,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2755.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 697.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.21,
|
||||
"mspt": {
|
||||
"average": 8.79,
|
||||
"p50": 8.05,
|
||||
"p95": 17.35,
|
||||
"p99": 30.47
|
||||
},
|
||||
"mspt_worst_p99": 48.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 781.88,
|
||||
"max": 782.8
|
||||
},
|
||||
"cpu_percent": 119.16,
|
||||
"chunks_per_second": 94.63,
|
||||
"unique_per_second": 3.58,
|
||||
"forgotten_per_second": 94.31,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 26003,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2600.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 679.9
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.14,
|
||||
"mspt": {
|
||||
"average": 18.67,
|
||||
"p50": 15.8,
|
||||
"p95": 38.26,
|
||||
"p99": 55.93
|
||||
},
|
||||
"mspt_worst_p99": 101.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3497.35,
|
||||
"max": 3499.4
|
||||
},
|
||||
"cpu_percent": 100.8,
|
||||
"chunks_per_second": 101.1,
|
||||
"unique_per_second": 4.38,
|
||||
"forgotten_per_second": 101.15,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 26537,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2653.7
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 687.5
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 5,
|
||||
"moved_too_quickly": 5,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.82,
|
||||
"mspt": {
|
||||
"average": 15.01,
|
||||
"p50": 13.12,
|
||||
"p95": 31.12,
|
||||
"p99": 50.62
|
||||
},
|
||||
"mspt_worst_p99": 101.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 799.52,
|
||||
"max": 799.7
|
||||
},
|
||||
"cpu_percent": 161.02,
|
||||
"chunks_per_second": 234.3,
|
||||
"unique_per_second": 8.49,
|
||||
"forgotten_per_second": 234.35,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 65926,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2637.04
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 683.96
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.95,
|
||||
"mspt": {
|
||||
"average": 18.78,
|
||||
"p50": 17.47,
|
||||
"p95": 28.16,
|
||||
"p99": 51.03
|
||||
},
|
||||
"mspt_worst_p99": 107.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3520.17,
|
||||
"max": 3573.8
|
||||
},
|
||||
"cpu_percent": 154.9,
|
||||
"chunks_per_second": 252.39,
|
||||
"unique_per_second": 11.89,
|
||||
"forgotten_per_second": 251.75,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 66469,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2658.76
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 689.68
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 10,
|
||||
"moved_too_quickly": 10,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.52,
|
||||
"mspt": {
|
||||
"average": 22.72,
|
||||
"p50": 16.98,
|
||||
"p95": 60.19,
|
||||
"p99": 84.16
|
||||
},
|
||||
"mspt_worst_p99": 105.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 813.5,
|
||||
"max": 815.8
|
||||
},
|
||||
"cpu_percent": 179.85,
|
||||
"chunks_per_second": 464.75,
|
||||
"unique_per_second": 16.94,
|
||||
"forgotten_per_second": 464.69,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 134839,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2696.78
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 685.48
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 16.36,
|
||||
"mspt": {
|
||||
"average": 34.3,
|
||||
"p50": 31.07,
|
||||
"p95": 55.55,
|
||||
"p99": 92.32
|
||||
},
|
||||
"mspt_worst_p99": 452.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3577.02,
|
||||
"max": 3611.2
|
||||
},
|
||||
"cpu_percent": 267.05,
|
||||
"chunks_per_second": 496.77,
|
||||
"unique_per_second": 26.74,
|
||||
"forgotten_per_second": 496.2,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 133505,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2670.1
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 688.8
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 39,
|
||||
"moved_too_quickly": 39,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 9.51,
|
||||
"mspt": {
|
||||
"average": 63.33,
|
||||
"p50": 58.89,
|
||||
"p95": 125.04,
|
||||
"p99": 175.12
|
||||
},
|
||||
"mspt_worst_p99": 261.5,
|
||||
"tps": 15.79,
|
||||
"rss_mb": {
|
||||
"mean": 868.12,
|
||||
"max": 869.9
|
||||
},
|
||||
"cpu_percent": 245.64,
|
||||
"chunks_per_second": 678.18,
|
||||
"unique_per_second": 31.8,
|
||||
"forgotten_per_second": 676.4,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 219787,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2197.87
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 637.91
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 34
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.14,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.72,
|
||||
"mspt": {
|
||||
"average": 9.73,
|
||||
"p50": 8.75,
|
||||
"p95": 20.99,
|
||||
"p99": 34.17
|
||||
},
|
||||
"mspt_worst_p99": 73.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 836.18,
|
||||
"max": 891.0
|
||||
},
|
||||
"cpu_percent": 329.64,
|
||||
"chunks_per_second": 41.11,
|
||||
"unique_per_second": 35.96,
|
||||
"forgotten_per_second": 41.08,
|
||||
"waited_ticks_per_second": 0.16,
|
||||
"chunks_total": 9460,
|
||||
"chunks_per_bot": {
|
||||
"explore": 9460.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 8479.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.54,
|
||||
"mspt": {
|
||||
"average": 9.71,
|
||||
"p50": 7.86,
|
||||
"p95": 20.37,
|
||||
"p99": 76.15
|
||||
},
|
||||
"mspt_worst_p99": 388.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3614.97,
|
||||
"max": 3688.5
|
||||
},
|
||||
"cpu_percent": 247.15,
|
||||
"chunks_per_second": 35.47,
|
||||
"unique_per_second": 33.61,
|
||||
"forgotten_per_second": 35.45,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 7912,
|
||||
"chunks_per_bot": {
|
||||
"explore": 7912.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 7580.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 21.27,
|
||||
"p50": 19.5,
|
||||
"p95": 39.15,
|
||||
"p99": 54.23
|
||||
},
|
||||
"mspt_worst_p99": 105.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1177.66,
|
||||
"max": 1392.9
|
||||
},
|
||||
"cpu_percent": 321.97,
|
||||
"chunks_per_second": 46.47,
|
||||
"unique_per_second": 46.11,
|
||||
"forgotten_per_second": 43.12,
|
||||
"waited_ticks_per_second": 111.25,
|
||||
"chunks_total": 14103,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1410.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1402.4
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 9856,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.24,
|
||||
"mspt": {
|
||||
"average": 16.94,
|
||||
"p50": 14.24,
|
||||
"p95": 30.28,
|
||||
"p99": 109.7
|
||||
},
|
||||
"mspt_worst_p99": 1861.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3953.29,
|
||||
"max": 4232.0
|
||||
},
|
||||
"cpu_percent": 254.11,
|
||||
"chunks_per_second": 24.99,
|
||||
"unique_per_second": 25.01,
|
||||
"forgotten_per_second": 25.33,
|
||||
"waited_ticks_per_second": 77.93,
|
||||
"chunks_total": 7707,
|
||||
"chunks_per_bot": {
|
||||
"explore": 770.7
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 769.5
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 11123,
|
||||
"moved_too_quickly": 20,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.92,
|
||||
"mspt": {
|
||||
"average": 25.76,
|
||||
"p50": 23.58,
|
||||
"p95": 45.01,
|
||||
"p99": 61.43
|
||||
},
|
||||
"mspt_worst_p99": 108.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1172.1,
|
||||
"max": 1482.3
|
||||
},
|
||||
"cpu_percent": 314.47,
|
||||
"chunks_per_second": 61.38,
|
||||
"unique_per_second": 60.7,
|
||||
"forgotten_per_second": 55.11,
|
||||
"waited_ticks_per_second": 304.54,
|
||||
"chunks_total": 24085,
|
||||
"chunks_per_bot": {
|
||||
"explore": 963.4
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 954.76
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 31264,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.76,
|
||||
"mspt": {
|
||||
"average": 24.93,
|
||||
"p50": 22.04,
|
||||
"p95": 37.09,
|
||||
"p99": 168.46
|
||||
},
|
||||
"mspt_worst_p99": 1724.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4040.42,
|
||||
"max": 4447.6
|
||||
},
|
||||
"cpu_percent": 305.47,
|
||||
"chunks_per_second": 37.91,
|
||||
"unique_per_second": 37.84,
|
||||
"forgotten_per_second": 38.39,
|
||||
"waited_ticks_per_second": 258.66,
|
||||
"chunks_total": 16865,
|
||||
"chunks_per_bot": {
|
||||
"explore": 674.6
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 671.28
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 33467,
|
||||
"moved_too_quickly": 23,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.41,
|
||||
"mspt": {
|
||||
"average": 39.31,
|
||||
"p50": 36.98,
|
||||
"p95": 65.33,
|
||||
"p99": 88.36
|
||||
},
|
||||
"mspt_worst_p99": 157.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1250.32,
|
||||
"max": 1517.1
|
||||
},
|
||||
"cpu_percent": 324.77,
|
||||
"chunks_per_second": 114.99,
|
||||
"unique_per_second": 112.25,
|
||||
"forgotten_per_second": 104.41,
|
||||
"waited_ticks_per_second": 543.77,
|
||||
"chunks_total": 50402,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1008.04
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 997.42
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 64163,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.55,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 9.31,
|
||||
"mspt": {
|
||||
"average": 52.7,
|
||||
"p50": 49.49,
|
||||
"p95": 85.73,
|
||||
"p99": 115.34
|
||||
},
|
||||
"mspt_worst_p99": 219.0,
|
||||
"tps": 18.97,
|
||||
"rss_mb": {
|
||||
"mean": 1358.32,
|
||||
"max": 3047.2
|
||||
},
|
||||
"cpu_percent": 327.95,
|
||||
"chunks_per_second": 154.2,
|
||||
"unique_per_second": 151.83,
|
||||
"forgotten_per_second": 141.65,
|
||||
"waited_ticks_per_second": 1132.9,
|
||||
"chunks_total": 84433,
|
||||
"chunks_per_bot": {
|
||||
"explore": 844.33
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 839.47
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 116161,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 35
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 16.47,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
# Horizon's server against vanilla 26.3: mixed bots, equal work (2026-10-18)
|
||||
|
||||
This reruns `../server-mixed-2026-10-18/` with the bots' load made the same on both servers. In that run the
|
||||
`explore` bots flew as fast as each server sent them chunks. Horizon kept up and vanilla didn't, so Horizon
|
||||
sent 2.9 to 5.9 times as many chunks a second, and vanilla's walking bots got a fifth of the chunks Horizon's
|
||||
got. The comparison of tick times wasn't fair.
|
||||
|
||||
## Summary
|
||||
|
||||
- **The work was equal** at 1 and 10 bots. At 25 bots it was equal in chunks sent a second (158.6 against 157.6);
|
||||
vanilla was still catching up on chunks its bots hadn't had yet, see "Equal work" below. Every bot joined, none
|
||||
was dropped, no chunk was sent twice, and no bot ever waited for the chunk it stood in.
|
||||
- **Tick time (MSPT), same work:**
|
||||
- 1 bot: 1.8 ms on Horizon against 11.3 on vanilla;
|
||||
- 10 bots: 4.7 against 34.3 ms;
|
||||
- 25 bots: 11.7 against 50.5 ms. Vanilla dropped just under 20 TPS (19.8) and its worst 5-second P99 was
|
||||
1388 ms, against Horizon's 130;
|
||||
- 50 bots: Horizon ran at 23.8 ms (20 TPS). Vanilla's watchdog stopped its server 30 s after the last bot
|
||||
joined ("A single server tick took 60.01 seconds").
|
||||
- **Memory:** Horizon's RSS was 4.1 to 4.9 times smaller: 0.69 against 3.40 GB with one bot, 0.95 against
|
||||
3.87 GB with 25, and 1.03 GB with 50. Vanilla's heap is fixed at 4 GB, so its RSS is mostly that heap.
|
||||
- **CPU** (100 % is one of the machine's 4 threads): 30 against 128 % at 10 bots, 58 against 192 % at 25.
|
||||
- **What changed since the first run:** the first run's vanilla held 50 bots at 29.7 ms because its walking
|
||||
bots got 640 to 1100 chunks each. With the explore bots paced, vanilla serves the walking bots in full
|
||||
(5400 to 6100 chunks each, against Horizon's 6100 to 6600), and that is the load it can't carry at 50.
|
||||
|
||||
## Results
|
||||
|
||||
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and P99
|
||||
are over the 300 measured seconds. "Worst P99" is the highest P99 of a single 5-second query. RSS and CPU
|
||||
cover the server's process tree. "First-time chunks/s" counts chunks a bot hadn't received before (summed
|
||||
over the bots). "Waited" is ticks a second that bots stood still waiting for their own chunk. "Pulled back"
|
||||
counts position packets that moved a bot back.
|
||||
|
||||
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | First-time chunks/s | Waited | Pulled back |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | **1.77** | 4.21 | 13.04 | 47.5 | 20 | **0.69 / 0.69 GB** | 28 % | 19.7 s | 9.94 | 0.01 | 0 | 0 |
|
||||
| 1 | vanilla | 11.34 | 17.12 | 33.22 | 119.1 | 20 | 3.40 / 3.42 GB | 36 % | 20.4 s | 9.98 | 0.01 | 0 | 0 |
|
||||
| 10 | Horizon | **4.74** | 10.48 | 19.69 | 120.1 | 20 | **0.86 / 0.88 GB** | 30 % | 19.5 s | 61.35 | 1.79 | 0 | 4159 |
|
||||
| 10 | vanilla | 34.30 | 53.72 | 89.08 | 375.8 | 20 | 3.71 / 3.75 GB | 128 % | 19.9 s | 61.66 | 1.93 | 0 | 3976 |
|
||||
| 25 | Horizon | **11.72** | 24.45 | 43.79 | 129.8 | 20 | **0.95 / 1.09 GB** | 58 % | 9.6 s | 158.57 | 5.78 | 0 | 3496 |
|
||||
| 25 | vanilla | 50.45 | 74.09 | 191.21 | 1388.1 | 19.82 | 3.87 / 4.75 GB | 192 % | 37.8 s | 157.63 | 8.25 | 0 | 3629 |
|
||||
| 50 | Horizon | **23.82** | 50.98 | 74.64 | 195.9 | 20 | **1.03 / 1.20 GB** | 98 % | 14.9 s | 302.81 | 10.96 | 0 | 7565 |
|
||||
| 50 | vanilla | stopped by its watchdog | | | | | | | 11.2 s | | | | |
|
||||
|
||||
Chunks each bot received over the whole run, by behaviour (mean):
|
||||
|
||||
| Bots | Server | walk | explore | idle |
|
||||
|---:|---|---:|---:|---:|
|
||||
| 1 | Horizon | 6092 | | |
|
||||
| 1 | vanilla | 6084 | | |
|
||||
| 10 | Horizon | 6248 | 1165 | 473 |
|
||||
| 10 | vanilla | 5729 | 1124 | 473 |
|
||||
| 25 | Horizon | 6629 | 1267 | 473 |
|
||||
| 25 | vanilla | 5387 | 1119 | 473 |
|
||||
| 50 | Horizon | 6497 | 1256 | 473 |
|
||||
|
||||
Both servers pulled bots back about as often (`moved too quickly` 20 and 296 times on vanilla at 10 and 25
|
||||
bots).
|
||||
|
||||
## Equal work
|
||||
|
||||
`tools/server_bench.py` now prints a work table after the runs. At each bot count it compares every server's
|
||||
chunks sent a second and first-time chunks a second with the busiest server's. Work counts as equal when
|
||||
both are within 10 % (or within a chunk a second when there are few). The report keeps the table as `work`.
|
||||
|
||||
| Bots | Chunks/s (Horizon, vanilla) | First-time chunks/s (Horizon, vanilla) | Equal |
|
||||
|---:|---|---|---|
|
||||
| 1 | 9.94, 9.98 | 0.01, 0.01 | yes |
|
||||
| 10 | 61.35, 61.66 | 1.79, 1.93 | yes |
|
||||
| 25 | 158.57, 157.63 | 5.78, 8.25 | no: vanilla sent 2.5 more first-time chunks a second |
|
||||
|
||||
Two settings made the load the same:
|
||||
|
||||
- **Explore bots paced at 0.8 blocks a second** (`--explore-speed 0.8`), the rate the 2026-10-17 run showed
|
||||
both servers keep up with. Every bot then follows the same path on both servers. The path is deterministic
|
||||
per bot, and a bot only stops while the chunk it stands in hasn't arrived, which never happened here
|
||||
(`Waited` 0). So both servers had the same chunks to send.
|
||||
- **A 300-second warm-up** before the 300 measured seconds. A first pass with the old 60-second warm-up
|
||||
(aborted after 10 bots) sent the same chunks a second on both (63.1 against 61.4). But vanilla was still
|
||||
delivering the bots' first views during the measurement (12.9 first-time chunks a second against Horizon's
|
||||
4.1), so its window held more work than Horizon's. With 300 seconds both were in steady state at 1 and 10
|
||||
bots. At 25, vanilla still sent 2.5 more first-time chunks a second than Horizon: 1.6 % of all it sent, and
|
||||
its walking bots ended the run with 19 % fewer chunks. Read the 25-bot tick times with that in mind: the
|
||||
extra first-time chunks are work vanilla had still to do because it fell behind.
|
||||
|
||||
## Method
|
||||
|
||||
`tools/server_bench.py` ran each configuration on a fresh server:
|
||||
|
||||
- the same `server.properties` on both: seed 12345, view and simulation distance 10, offline, creative,
|
||||
difficulty easy, `sync-chunk-writes=true`, white-list off;
|
||||
- `gamerule respawn_radius 0`, and otherwise vanilla's default game rules (the locator bar on for both);
|
||||
- the server waits for `Done`, the bots join one a second, warm up for 300 seconds and are measured for 300,
|
||||
with `/tick query` every 5 seconds and RSS and CPU every second.
|
||||
|
||||
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice deterministic
|
||||
per bot. All are creative and fly 24 blocks above spawn. `walk` flies to random points within 48 blocks at
|
||||
5.6 blocks a second, `explore` flies straight out at 0.8 blocks a second, and `idle` hovers.
|
||||
|
||||
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS. The desktop (24 threads)
|
||||
was meant to run it, but it didn't wake (`~/fleet/power wake desktop` timed out, as it did for the first run).
|
||||
No other session ran on the ThinkPad during the runs (`load.txt`, a sample a minute). The servers ran under
|
||||
`nice -n 19`. The bots ran on the Mac mini (M4 Pro, shared with other work at 70 to 90 % CPU) and reached the
|
||||
ThinkPad over the tailnet.
|
||||
|
||||
**What ran.**
|
||||
|
||||
- Horizon: the server build from #916 (`horizon-client --no-default-features`, no sound or controllers) at
|
||||
519ec121, packaged with `xtask package-server` and installed with `install.sh --archive`, as Linux owners
|
||||
will get it. `horizon-server` ran it as `horizon-engine`. The game files were prepared from the owner's own
|
||||
Minecraft. `horizon-bots` and `server_bench.py` came from this PR.
|
||||
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
|
||||
|
||||
`server_bench.py` stopped after vanilla's 50-bot server crashed and before it wrote its report.
|
||||
`mixed-equal.json` holds the run lines it printed. The vanilla 50 entry and its error come from that server's
|
||||
log, and the `work` table was computed with `server_bench.work`. `load.txt` is the fleet monitor's view of the
|
||||
machines during the runs.
|
||||
|
||||
## Reproduce
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
|
||||
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
|
||||
cargo build --release -p horizon-bots # on the machine that runs the bots
|
||||
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
|
||||
HORIZON_BENCH_BIN=~/.local/share/horizon/current \
|
||||
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50 --behaviour mixed \
|
||||
--explore-speed 0.8 --warmup 300 --nice 19 \
|
||||
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
|
||||
--out mixed-equal.json
|
||||
|
||||
## Next
|
||||
|
||||
- One pass per configuration ran. Vanilla at 50 bots, in particular, needs a second pass, ideally on the
|
||||
desktop (24 threads), where neither server is CPU-bound.
|
||||
- 100 bots, and Paper and Fabric + Lithium (docs/server.md section 9).
|
||||
- A longer warm-up, or measuring from when the first-time chunk rate settles, so the 25-bot work is equal too.
|
||||
@@ -1,702 +0,0 @@
|
||||
2026-10-18T19:32:39Z
|
||||
ludothinkpad ok: 27.2 GB free, 4 threads 17% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.9 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 27.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 42% busy (agents 0.0, rest 1.6 cores) · 80°C · load 5.9
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:33:44Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 19% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 93% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 34% busy (agents 0.0, rest 1.7 cores) · 81°C · load 4.0
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:34:48Z
|
||||
ludothinkpad ok: 26.4 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.9 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 54% busy (agents 0.0, rest 2.2 cores) · 79°C · load 2.8
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:35:52Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.4 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 30% busy (agents 0.0, rest 1.1 cores) · 82°C · load 2.2
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:36:57Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 25% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.4 GB free, 14 threads 93% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 37% busy (agents 0.0, rest 1.2 cores) · 80°C · load 1.5
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:38:01Z
|
||||
ludothinkpad ok: 26.6 GB free, 4 threads 54% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.7 GB free, 14 threads 64% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 21% busy (agents 0.0, rest 1.2 cores) · 81°C · load 1.9
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:39:05Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 20% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.1 GB free, 14 threads 70% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 28% busy (agents 0.0, rest 1.0 cores) · 81°C · load 1.3
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:40:10Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 39% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.2 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 32% busy (agents 0.0, rest 1.7 cores) · 80°C · load 0.9
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:41:14Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 31% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.8 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 15% busy (agents 0.0, rest 1.1 cores) · 82°C · load 1.3
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:42:19Z
|
||||
ludothinkpad ok: 26.5 GB free, 4 threads 26% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.1 GB free, 14 threads 94% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 30% busy (agents 0.0, rest 1.3 cores) · 78°C · load 1.1
|
||||
· disk 289.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:43:23Z
|
||||
ludothinkpad busy: 26.7 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.2 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 99% busy (agents 0.0, rest 3.8 cores) · 86°C · load 2.4
|
||||
· disk 289.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:44:27Z
|
||||
ludothinkpad busy: 25.2 GB free, 4 threads 98% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.0 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 24.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 83% busy (agents 0.0, rest 3.7 cores) · 83°C · load 5.5
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:45:31Z
|
||||
ludothinkpad ok: 24.5 GB free, 4 threads 39% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.0 GB free, 14 threads 93% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 24.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 34% busy (agents 0.0, rest 1.5 cores) · 84°C · load 3.5
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:46:35Z
|
||||
ludothinkpad ok: 23.7 GB free, 4 threads 69% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 23.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 75% busy (agents 0.0, rest 2.8 cores) · 87°C · load 2.7
|
||||
· disk 289.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:47:40Z
|
||||
ludothinkpad ok: 23.8 GB free, 4 threads 66% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.9 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 23.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 59% busy (agents 0.0, rest 2.5 cores) · 83°C · load 2.9
|
||||
· disk 291.7 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:48:44Z
|
||||
ludothinkpad ok: 24.2 GB free, 4 threads 61% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.0 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 24.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 62% busy (agents 0.0, rest 2.5 cores) · 84°C · load 3.2
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:49:48Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 59% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.1 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 25.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 57% busy (agents 0.0, rest 2.4 cores) · 85°C · load 2.9
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:50:52Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 39% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.4 GB free, 14 threads 88% busy, 2 sessions
|
||||
macbook offline
|
||||
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 18% busy (agents 0.0, rest 1.4 cores) · 67°C · load 2.4
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:51:57Z
|
||||
ludothinkpad ok: 23.7 GB free, 4 threads 40% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.9 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 23% busy (agents 0.0, rest 1.4 cores) · 81°C · load 1.8
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:53:01Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 25% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 25.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 31% busy (agents 0.0, rest 1.2 cores) · 80°C · load 1.8
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:54:05Z
|
||||
ludothinkpad ok: 26.5 GB free, 4 threads 75% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 69% busy (agents 0.0, rest 2.5 cores) · 78°C · load 2.3
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:55:09Z
|
||||
ludothinkpad busy: 26.4 GB free, 4 threads 89% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.3 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 32% busy (agents 0.0, rest 2.4 cores) · 79°C · load 2.9
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:56:14Z
|
||||
ludothinkpad ok: 26.4 GB free, 4 threads 39% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 68% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 63% busy (agents 0.0, rest 1.8 cores) · 82°C · load 2.3
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:57:18Z
|
||||
ludothinkpad ok: 26.3 GB free, 4 threads 20% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.2 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 19% busy (agents 0.0, rest 1.0 cores) · 80°C · load 1.8
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:58:22Z
|
||||
ludothinkpad ok: 26.3 GB free, 4 threads 28% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.3 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 93% busy (agents 0.0, rest 2.5 cores) · 80°C · load 2.0
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T19:59:27Z
|
||||
ludothinkpad ok: 26.2 GB free, 4 threads 52% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 35.0 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 22% busy (agents 0.0, rest 1.8 cores) · 80°C · load 3.6
|
||||
· disk 291.4 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:00:31Z
|
||||
ludothinkpad busy: 25.8 GB free, 4 threads 87% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 33.0 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
RAM 27.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 51% busy (agents 0.0, rest 2.9 cores) · 79°C · load 3.6
|
||||
· disk 291.5 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:01:35Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 35% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 32.4 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook ok: 7.2 GB free, 12 threads 32% busy, 0 sessions
|
||||
RAM 25.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 34% busy (agents 0.0, rest 1.3 cores) · 81°C · load 2.5
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:02:40Z
|
||||
ludothinkpad ok: 26.6 GB free, 4 threads 35% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.2 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook ok: 6.5 GB free, 12 threads 39% busy, 0 sessions
|
||||
RAM 26.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 33% busy (agents 0.0, rest 1.3 cores) · 81°C · load 2.7
|
||||
· disk 291.6 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:03:44Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.1 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook ok: 6.8 GB free, 12 threads 6% busy, 0 sessions
|
||||
RAM 25.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 66% busy (agents 0.0, rest 2.5 cores) · 87°C · load 2.5
|
||||
· disk 291.3 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:04:48Z
|
||||
ludothinkpad busy: 26.2 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 30.0 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 31% busy, 0 sessions
|
||||
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 84% busy (agents 0.0, rest 3.7 cores) · 85°C · load 3.4
|
||||
· disk 291.3 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:05:52Z
|
||||
ludothinkpad busy: 24.5 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 34.1 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 24.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 100% busy (agents 0.0, rest 4.0 cores) · 83°C · load 9.0
|
||||
· disk 291.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:06:57Z
|
||||
ludothinkpad busy: 23.5 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.3 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 5% busy, 0 sessions
|
||||
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 10.1
|
||||
· disk 309.2 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:08:01Z
|
||||
ludothinkpad busy: 23.4 GB free, 4 threads 88% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.6 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 2% busy, 0 sessions
|
||||
RAM 23.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 77% busy (agents 0.0, rest 3.4 cores) · 84°C · load 7.5
|
||||
· disk 309.2 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:09:05Z
|
||||
ludothinkpad busy: 24.0 GB free, 4 threads 89% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.9 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook ok: 7.1 GB free, 12 threads 19% busy, 0 sessions
|
||||
RAM 23.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 91% busy (agents 0.0, rest 3.6 cores) · 82°C · load 5.9
|
||||
· disk 309.1 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:10:09Z
|
||||
ludothinkpad busy: 24.0 GB free, 4 threads 98% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.1 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 19% busy, 0 sessions
|
||||
RAM 24.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 92% busy (agents 0.0, rest 3.7 cores) · 84°C · load 6.6
|
||||
· disk 309.1 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:11:14Z
|
||||
ludothinkpad busy: 24.3 GB free, 4 threads 92% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.3 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 18% busy, 0 sessions
|
||||
RAM 24.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 99% busy (agents 0.0, rest 3.7 cores) · 85°C · load 6.5
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:12:18Z
|
||||
ludothinkpad busy: 24.7 GB free, 4 threads 89% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.6 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook ok: 7.3 GB free, 12 threads 20% busy, 0 sessions
|
||||
RAM 24.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 82% busy (agents 0.0, rest 3.4 cores) · 84°C · load 6.1
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:13:22Z
|
||||
ludothinkpad ok: 23.4 GB free, 4 threads 53% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.1 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook ok: 7.3 GB free, 12 threads 20% busy, 0 sessions
|
||||
RAM 23.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 48% busy (agents 0.0, rest 2.4 cores) · 80°C · load 4.2
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:14:27Z
|
||||
ludothinkpad ok: 24.0 GB free, 4 threads 44% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.0 GB free, 14 threads 71% busy, 1 sessions
|
||||
macbook ok: 6.9 GB free, 12 threads 2% busy, 0 sessions
|
||||
RAM 24.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 23% busy (agents 0.0, rest 2.0 cores) · 77°C · load 4.3
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:15:31Z
|
||||
ludothinkpad ok: 22.7 GB free, 4 threads 31% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.2 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook ok: 6.9 GB free, 12 threads 19% busy, 0 sessions
|
||||
RAM 22.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 30% busy (agents 0.0, rest 1.3 cores) · 77°C · load 2.6
|
||||
· disk 309.1 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:16:35Z
|
||||
ludothinkpad ok: 26.8 GB free, 4 threads 37% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.6 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook ok: 6.8 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 26.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 42% busy (agents 0.0, rest 1.7 cores) · 81°C · load 2.3
|
||||
· disk 309.1 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:17:39Z
|
||||
ludothinkpad ok: 26.3 GB free, 4 threads 63% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook ok: 7.0 GB free, 12 threads 19% busy, 0 sessions
|
||||
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 64% busy (agents 0.0, rest 2.6 cores) · 87°C · load 1.9
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:18:43Z
|
||||
ludothinkpad ok: 26.9 GB free, 4 threads 37% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 32.2 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook ok: 7.0 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 26.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 56% busy (agents 0.0, rest 1.9 cores) · 84°C · load 3.9
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:19:48Z
|
||||
ludothinkpad ok: 26.0 GB free, 4 threads 77% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.2 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook ok: 7.0 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 26.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 66% busy (agents 0.0, rest 2.9 cores) · 86°C · load 3.2
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:20:52Z
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 80% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook ok: 7.0 GB free, 12 threads 17% busy, 0 sessions
|
||||
RAM 25.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 68% busy (agents 0.0, rest 3.2 cores) · 86°C · load 4.4
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:21:56Z
|
||||
ludothinkpad ok: 26.3 GB free, 4 threads 63% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.3 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook ok: 6.8 GB free, 12 threads 14% busy, 0 sessions
|
||||
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 66% busy (agents 0.0, rest 2.8 cores) · 84°C · load 3.1
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:23:00Z
|
||||
ludothinkpad ok: 26.8 GB free, 4 threads 74% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.1 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 11% busy, 0 sessions
|
||||
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 65% busy (agents 0.0, rest 2.8 cores) · 84°C · load 5.0
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:24:05Z
|
||||
ludothinkpad ok: 26.9 GB free, 4 threads 64% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.3 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook ok: 6.8 GB free, 12 threads 8% busy, 0 sessions
|
||||
RAM 26.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 64% busy (agents 0.0, rest 2.7 cores) · 86°C · load 4.9
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:25:09Z
|
||||
ludothinkpad ok: 26.0 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 12% busy, 0 sessions
|
||||
RAM 26.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 49% busy (agents 0.0, rest 2.1 cores) · 72°C · load 5.5
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:26:13Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 47% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.8 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
|
||||
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 56% busy (agents 0.0, rest 2.1 cores) · 78°C · load 3.1
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:27:18Z
|
||||
ludothinkpad busy: 25.5 GB free, 4 threads 94% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.6 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
|
||||
RAM 25.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 100% busy (agents 0.0, rest 3.9 cores) · 78°C · load 4.7
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:28:22Z
|
||||
ludothinkpad busy: 24.6 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.4 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
|
||||
RAM 24.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 77°C · load 10.5
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:29:27Z
|
||||
ludothinkpad busy: 24.3 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.8 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
|
||||
RAM 24.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 10.0
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:30:31Z
|
||||
ludothinkpad busy: 23.7 GB free, 4 threads 91% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.1 GB free, 14 threads 71% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 94% busy (agents 0.0, rest 3.8 cores) · 80°C · load 10.3
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:31:36Z
|
||||
ludothinkpad busy: 23.6 GB free, 4 threads 93% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.0 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 14% busy, 0 sessions
|
||||
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 95% busy (agents 0.0, rest 3.7 cores) · 80°C · load 10.7
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:32:40Z
|
||||
ludothinkpad busy: 23.6 GB free, 4 threads 94% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.9 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook ok: 6.8 GB free, 12 threads 22% busy, 0 sessions
|
||||
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 84% busy (agents 0.0, rest 3.5 cores) · 80°C · load 9.7
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:33:45Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 51% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
|
||||
RAM 25.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 48% busy (agents 0.0, rest 2.1 cores) · 87°C · load 6.5
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:34:49Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.2 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
|
||||
RAM 24.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 78% busy (agents 0.0, rest 2.7 cores) · 88°C · load 4.0
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:35:53Z
|
||||
ludothinkpad busy: 23.6 GB free, 4 threads 86% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.9 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
|
||||
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 88% busy (agents 0.0, rest 3.3 cores) · 85°C · load 4.9
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:36:58Z
|
||||
ludothinkpad ok: 23.6 GB free, 4 threads 75% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.4 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook ok: 6.5 GB free, 12 threads 18% busy, 0 sessions
|
||||
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 85% busy (agents 0.0, rest 3.2 cores) · 85°C · load 5.1
|
||||
· disk 309.0 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:38:02Z
|
||||
ludothinkpad ok: 22.6 GB free, 4 threads 55% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.5 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 22.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 54% busy (agents 0.0, rest 2.2 cores) · 85°C · load 4.5
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:39:06Z
|
||||
ludothinkpad ok: 27.2 GB free, 4 threads 55% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.5 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 27.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 48% busy (agents 0.0, rest 2.2 cores) · 87°C · load 4.0
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:40:10Z
|
||||
ludothinkpad ok: 26.2 GB free, 4 threads 72% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.7 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 19% busy, 0 sessions
|
||||
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 80% busy (agents 0.0, rest 3.1 cores) · 86°C · load 10.8
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:41:15Z
|
||||
ludothinkpad busy: 26.4 GB free, 4 threads 86% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.5 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 90% busy (agents 0.0, rest 3.4 cores) · 86°C · load 12.8
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:42:19Z
|
||||
ludothinkpad ok: 26.2 GB free, 4 threads 82% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.9 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 11% busy, 0 sessions
|
||||
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 74% busy (agents 0.0, rest 3.3 cores) · 85°C · load 8.8
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:43:23Z
|
||||
ludothinkpad busy: 26.7 GB free, 4 threads 93% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 88% busy (agents 0.0, rest 3.4 cores) · 85°C · load 5.4
|
||||
· disk 308.9 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:44:27Z
|
||||
ludothinkpad ok: 27.0 GB free, 4 threads 82% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.9 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 13% busy, 0 sessions
|
||||
RAM 27.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 75% busy (agents 0.0, rest 3.3 cores) · 82°C · load 9.8
|
||||
· disk 308.8 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:45:32Z
|
||||
ludothinkpad ok: 27.2 GB free, 4 threads 76% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 84% busy (agents 0.0, rest 3.2 cores) · 85°C · load 6.9
|
||||
· disk 308.8 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:46:36Z
|
||||
ludothinkpad busy: 27.1 GB free, 4 threads 87% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.8 GB free, 14 threads 92% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 26.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 85% busy (agents 0.0, rest 3.2 cores) · 82°C · load 4.4
|
||||
· disk 308.8 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:47:40Z
|
||||
ludothinkpad busy: 25.3 GB free, 4 threads 86% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.5 GB free, 14 threads 72% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 25.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 85% busy (agents 0.0, rest 3.4 cores) · 86°C · load 3.7
|
||||
· disk 308.8 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:48:45Z
|
||||
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.5 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 15% busy, 0 sessions
|
||||
RAM 26.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 98% busy (agents 0.0, rest 3.9 cores) · 80°C · load 6.5
|
||||
· disk 308.8 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:49:50Z
|
||||
ludothinkpad ok: 26.8 GB free, 4 threads 71% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.7 GB free, 14 threads 93% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 92% busy (agents 0.0, rest 3.4 cores) · 81°C · load 5.2
|
||||
· disk 308.7 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:50:54Z
|
||||
ludothinkpad busy: 25.8 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.3 GB free, 14 threads 95% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 11% busy, 0 sessions
|
||||
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 8.1
|
||||
· disk 308.7 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:51:59Z
|
||||
ludothinkpad ok: 27.2 GB free, 4 threads 12% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.2 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
|
||||
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 15% busy (agents 0.0, rest 1.3 cores) · 81°C · load 7.9
|
||||
· disk 308.7 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:53:03Z
|
||||
ludothinkpad ok: 27.3 GB free, 4 threads 42% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 14% busy (agents 0.0, rest 0.8 cores) · 82°C · load 3.1
|
||||
· disk 308.7 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:54:08Z
|
||||
ludothinkpad ok: 28.9 GB free, 4 threads 4% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.2 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
|
||||
RAM 28.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 2% busy (agents 0.0, rest 0.3 cores) · 82°C · load 1.3
|
||||
· disk 308.7 GB free · battery 96% · Ludovic: away
|
||||
|
||||
2026-10-18T20:55:12Z
|
||||
ludothinkpad ok: 27.7 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.2 GB free, 14 threads 70% busy, 1 sessions
|
||||
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
|
||||
RAM 27.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 26% busy (agents 0.0, rest 1.2 cores) · 69°C · load 1.0
|
||||
· disk 308.8 GB free · battery 96% · Ludovic: away
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-18T19:32:00+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4,
|
||||
"name": "ThinkPad, Intel Core i7-7500U, 32 GB, NixOS"
|
||||
},
|
||||
"commit": "f1d5bf520986b00a5b968a84d85809fca1a039fa",
|
||||
"engine": "horizon-engine: the server build of #916 (horizon-client --no-default-features) at 519ec121, as install.sh --archive installed it",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50",
|
||||
"behaviour": "mixed",
|
||||
"join_every": 1.0,
|
||||
"warmup": 300,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0.8,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"connect": "the ThinkPad's tailnet address",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"note": "server_bench.py was stopped after vanilla's 50-bot server crashed, before it wrote this file; the runs are the lines it printed, and the vanilla 50 entry is taken from that server's log.",
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 19.73,
|
||||
"mspt": {
|
||||
"average": 1.77,
|
||||
"p50": 1.34,
|
||||
"p95": 4.21,
|
||||
"p99": 13.04
|
||||
},
|
||||
"mspt_worst_p99": 47.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 686.54,
|
||||
"max": 686.7
|
||||
},
|
||||
"cpu_percent": 27.92,
|
||||
"chunks_per_second": 9.94,
|
||||
"unique_per_second": 0.01,
|
||||
"forgotten_per_second": 9.9,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 6092,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6092.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 20.45,
|
||||
"mspt": {
|
||||
"average": 11.34,
|
||||
"p50": 10.6,
|
||||
"p95": 17.12,
|
||||
"p99": 33.22
|
||||
},
|
||||
"mspt_worst_p99": 119.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3404.16,
|
||||
"max": 3416.3
|
||||
},
|
||||
"cpu_percent": 35.93,
|
||||
"chunks_per_second": 9.98,
|
||||
"unique_per_second": 0.01,
|
||||
"forgotten_per_second": 9.93,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 6084,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6084.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 19.54,
|
||||
"mspt": {
|
||||
"average": 4.74,
|
||||
"p50": 3.83,
|
||||
"p95": 10.48,
|
||||
"p99": 19.69
|
||||
},
|
||||
"mspt_worst_p99": 120.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 855.26,
|
||||
"max": 882.7
|
||||
},
|
||||
"cpu_percent": 29.78,
|
||||
"chunks_per_second": 61.35,
|
||||
"unique_per_second": 1.79,
|
||||
"forgotten_per_second": 61.47,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 40762,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6247.67,
|
||||
"explore": 1165.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 701.83,
|
||||
"explore": 1086.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 4159,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 19.95,
|
||||
"mspt": {
|
||||
"average": 34.3,
|
||||
"p50": 32.2,
|
||||
"p95": 53.72,
|
||||
"p99": 89.08
|
||||
},
|
||||
"mspt_worst_p99": 375.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3713.5,
|
||||
"max": 3754.9
|
||||
},
|
||||
"cpu_percent": 128.07,
|
||||
"chunks_per_second": 61.66,
|
||||
"unique_per_second": 1.93,
|
||||
"forgotten_per_second": 61.71,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 37568,
|
||||
"chunks_per_bot": {
|
||||
"walk": 5729.0,
|
||||
"explore": 1124.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 701.17,
|
||||
"explore": 1055.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3976,
|
||||
"moved_too_quickly": 20,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 9.61,
|
||||
"mspt": {
|
||||
"average": 11.72,
|
||||
"p50": 9.88,
|
||||
"p95": 24.45,
|
||||
"p99": 43.79
|
||||
},
|
||||
"mspt_worst_p99": 129.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 947.35,
|
||||
"max": 1086.1
|
||||
},
|
||||
"cpu_percent": 57.83,
|
||||
"chunks_per_second": 158.57,
|
||||
"unique_per_second": 5.78,
|
||||
"forgotten_per_second": 158.45,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 108126,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6628.53,
|
||||
"explore": 1266.6,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 704.67,
|
||||
"explore": 1189.2,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3496,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 37.78,
|
||||
"mspt": {
|
||||
"average": 50.45,
|
||||
"p50": 46.39,
|
||||
"p95": 74.09,
|
||||
"p99": 191.21
|
||||
},
|
||||
"mspt_worst_p99": 1388.1,
|
||||
"tps": 19.82,
|
||||
"rss_mb": {
|
||||
"mean": 3866.18,
|
||||
"max": 4745.0
|
||||
},
|
||||
"cpu_percent": 192.42,
|
||||
"chunks_per_second": 157.63,
|
||||
"unique_per_second": 8.25,
|
||||
"forgotten_per_second": 156.12,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 88763,
|
||||
"chunks_per_bot": {
|
||||
"walk": 5387.0,
|
||||
"explore": 1118.6,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0,
|
||||
"explore": 1075.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 2,
|
||||
"pulled_back": 3629,
|
||||
"moved_too_quickly": 296,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 14.93,
|
||||
"mspt": {
|
||||
"average": 23.82,
|
||||
"p50": 20.11,
|
||||
"p95": 50.98,
|
||||
"p99": 74.64
|
||||
},
|
||||
"mspt_worst_p99": 195.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1031.41,
|
||||
"max": 1201.2
|
||||
},
|
||||
"cpu_percent": 98.21,
|
||||
"chunks_per_second": 302.81,
|
||||
"unique_per_second": 10.96,
|
||||
"forgotten_per_second": 303.47,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 212181,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6496.53,
|
||||
"explore": 1255.5,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 701.9,
|
||||
"explore": 1184.7,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 7565,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 11.16,
|
||||
"error": "stopped by its watchdog 30 s after the last bot joined: \"A single server tick took 60.01 seconds (should be max 0.05)\" (Player.tick)"
|
||||
}
|
||||
],
|
||||
"work": [
|
||||
{
|
||||
"behaviour": "mixed",
|
||||
"bots": 1,
|
||||
"servers": {
|
||||
"horizon": {
|
||||
"chunks_per_second": {
|
||||
"value": 9.94,
|
||||
"share": 0.996
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 0.01,
|
||||
"share": 1.0
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"vanilla": {
|
||||
"chunks_per_second": {
|
||||
"value": 9.98,
|
||||
"share": 1.0
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 0.01,
|
||||
"share": 1.0
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
}
|
||||
},
|
||||
"equal": true
|
||||
},
|
||||
{
|
||||
"behaviour": "mixed",
|
||||
"bots": 10,
|
||||
"servers": {
|
||||
"horizon": {
|
||||
"chunks_per_second": {
|
||||
"value": 61.35,
|
||||
"share": 0.995
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 1.79,
|
||||
"share": 0.927
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"vanilla": {
|
||||
"chunks_per_second": {
|
||||
"value": 61.66,
|
||||
"share": 1.0
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 1.93,
|
||||
"share": 1.0
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
}
|
||||
},
|
||||
"equal": true
|
||||
},
|
||||
{
|
||||
"behaviour": "mixed",
|
||||
"bots": 25,
|
||||
"servers": {
|
||||
"horizon": {
|
||||
"chunks_per_second": {
|
||||
"value": 158.57,
|
||||
"share": 1.0
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 5.78,
|
||||
"share": 0.701
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"vanilla": {
|
||||
"chunks_per_second": {
|
||||
"value": 157.63,
|
||||
"share": 0.994
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 8.25,
|
||||
"share": 1.0
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
}
|
||||
},
|
||||
"equal": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
# Horizon's server against vanilla 26.3, equal work (2026-10-17)
|
||||
|
||||
The same bots on Horizon's dedicated server and on vanilla 26.3's, one server at a time, on the same
|
||||
machine, with the work each server does made the same before the numbers are compared. This replaces the
|
||||
first run in docs/server.md section 9, where the work wasn't equal.
|
||||
|
||||
## What was made equal
|
||||
|
||||
The first run (PR #556) had three differences that made the comparison unfair. Each is fixed in the code
|
||||
that ran here:
|
||||
|
||||
- **Refused moves.** Vanilla pulls a player back when a move is too fast ("moved too quickly") or ends
|
||||
inside a block ("moved wrongly"). Horizon now runs both checks the way vanilla does (PR #586), so a bot
|
||||
that flies too fast is pulled back on both servers.
|
||||
- **Chunk sending.** Vanilla paces chunks by the client's acknowledgements (PlayerChunkSender: one
|
||||
unacknowledged batch at first, then ten, each at most the client's desired chunks per tick). Horizon used
|
||||
to send twice as many per batch; it now paces them the same way (PR #586), and the bots acknowledge
|
||||
batches and ask for a rate the way vanilla's client does, so both servers send at the pace the client
|
||||
asks for. Chunks sent again while the bot still held
|
||||
them (`resent`) are counted: 0 on both servers in every run.
|
||||
- **Movement gated on chunks.** A bot doesn't move while the chunk it stands in hasn't arrived (vanilla's
|
||||
client waits the same way), so a slower server isn't asked to teleport bots into terrain it hasn't sent.
|
||||
|
||||
One difference isn't in Horizon yet: vanilla 26.3's locator bar (waypoints), which costs vanilla O(players²)
|
||||
work per move. Every vanilla run here sets `gamerule locator_bar false`, so neither server does it.
|
||||
|
||||
## Method
|
||||
|
||||
`tools/server_bench.py` runs each configuration on a fresh server:
|
||||
|
||||
- the same `server.properties` on both (seed 12345, view and simulation distance 10, offline, creative,
|
||||
difficulty easy, `sync-chunk-writes=true`), and `gamerule respawn_radius 0`;
|
||||
- the server waits for `Done`, the bots join one a second, a 60-second warm-up, then 300 seconds of
|
||||
measurement;
|
||||
- MSPT (average, P50, P95, P99) comes from each server's own `/tick query`, asked every 5 seconds on the
|
||||
console, so both measure themselves the same way (vanilla's ServerTickRateManager numbers, which Horizon
|
||||
reproduces); TPS is 20 while the average fits in 50 ms, else 1000 / MSPT;
|
||||
- RSS and CPU are the server's whole process tree, once a second (100 % is one core);
|
||||
- chunk counts come from the bots: chunks received per second, chunks no bot had before (new terrain), and
|
||||
pull-backs (position packets that move a bot back).
|
||||
|
||||
The bots are `horizon-bots` (creative, flying 24 blocks above spawn, 20 moves a second):
|
||||
|
||||
- `walk`: flies to random points within 48 blocks of spawn at sprint speed (5.6 blocks a second);
|
||||
- `explore`: flies straight outwards, by default at about 22 blocks a second, as fast as the server sends
|
||||
chunks.
|
||||
|
||||
Machine: the ThinkPad (Intel Core i7-7500U, 2 cores / 4 threads, 32 GB, NixOS), servers under `nice -n 19`
|
||||
because it also runs other services; vanilla on OpenJDK 25.0.4.1 with a 4 GB heap (`-Xms4g -Xmx4g`). The
|
||||
bots ran on the Mac mini and reached the ThinkPad over the tailnet, so they took none of the server's CPU.
|
||||
|
||||
What ran: Horizon from `origin/main` at bf851bfe with #586 (move checks and chunk pacing), the bots and
|
||||
`server_bench.py` from this PR, #623 (sections encoded once, its first commit) and #649 (a tick's packets
|
||||
handed to each connection together). The ThinkPad copy wasn't a git checkout, so the JSON reports have an
|
||||
empty `commit`. Vanilla: the pinned 26.3 server jar.
|
||||
|
||||
## Results
|
||||
|
||||
MSPT is the average milliseconds per tick (50 is the limit for 20 TPS); P95 is the 95th percentile. CPU is
|
||||
the whole process tree. "New chunks/s" are chunks no bot had before.
|
||||
|
||||
### Walk (the same work on both servers)
|
||||
|
||||
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 5.5 | 12.1 | 20.0 | 1.37 GB | 57 % | 9 | 0.3 |
|
||||
| 1 | Vanilla | 18.3 | 27.3 | 20.0 | 3.35 GB | 66 % | 10 | 0.3 |
|
||||
| 10 | Horizon | 11.5 | 22.1 | 20.0 | 1.41 GB | 124 % | 94 | 2.4 |
|
||||
| 10 | Vanilla | 16.8 | 30.5 | 20.0 | 3.41 GB | 84 % | 100 | 2.8 |
|
||||
| 25 | Horizon | 20.7 | 40.4 | 20.0 | 1.45 GB | 170 % | 236 | 5.7 |
|
||||
| 25 | Vanilla | 16.0 | 26.4 | 20.0 | 3.47 GB | 115 % | 252 | 6.7 |
|
||||
| 50 | Horizon | 31.4 | 79.0 | 20.0 | 1.52 GB | 173 % | 451 | 10.3 |
|
||||
| 50 | Vanilla | 20.5 | 29.7 | 20.0 | 3.55 GB | 194 % | 502 | 12.2 |
|
||||
| 100 | Horizon | 114.5 | 256.2 | 8.7 | 1.33 GB | 181 % | 484 | 30.8 |
|
||||
| 100 | Vanilla | stopped by its watchdog | | | | | | |
|
||||
|
||||
Both servers sent the same chunks (about 10 a second per bot, the churn of flying around) and sent none
|
||||
twice. Vanilla pulled back a few bots for moving too quickly when a tick ran long (5 to 33 per run);
|
||||
Horizon none. At 100 bots vanilla's watchdog stopped the server (a tick ran over 60 s, in
|
||||
`ChunkMap$TrackedEntity.updatePlayer`); Horizon kept running at 8.7 TPS.
|
||||
|
||||
### Explore at a rate both servers keep up with (equal work)
|
||||
|
||||
The bots fly outwards at 0.8 blocks a second (`--explore-speed 0.8`), slow enough that neither server makes
|
||||
them wait for terrain, so both generate and send the same new chunks.
|
||||
|
||||
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 4.0 | 6.6 | 20.0 | 1.36 GB | 25 % | 2 | 1.3 |
|
||||
| 1 | Vanilla | 18.1 | 27.1 | 20.0 | 3.32 GB | 73 % | 2 | 1.4 |
|
||||
| 10 | Horizon | 13.5 | 24.8 | 20.0 | 1.64 GB | 89 % | 13 | 11.8 |
|
||||
| 10 | Vanilla | 29.2 | 43.0 | 20.0 | 3.52 GB | 143 % | 13 | 11.9 |
|
||||
| 25 | Horizon | 20.2 | 36.8 | 20.0 | 1.74 GB | 121 % | 32 | 28.9 |
|
||||
| 25 | Vanilla | 37.0 | 49.4 | 20.0 | 3.73 GB | 169 % | 32 | 28.9 |
|
||||
|
||||
### Explore as fast as each server sends chunks (not equal work)
|
||||
|
||||
The default 22 blocks a second is faster than either server can generate, so each server sets the pace and
|
||||
a faster server is given more work: from 10 bots Horizon generated 1.5 to 2 times vanilla's new chunks a
|
||||
second. Read it as throughput, not as a like-for-like tick time.
|
||||
|
||||
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 10.6 | 24.6 | 20.0 | 1.34 GB | 322 % | 39 | 34.6 |
|
||||
| 1 | Vanilla | 9.9 | 21.3 | 20.0 | 3.48 GB | 245 % | 36 | 33.4 |
|
||||
| 10 | Horizon | 30.4 | 52.3 | 20.0 | 2.18 GB | 345 % | 47 | 46.7 |
|
||||
| 10 | Vanilla | 17.0 | 29.6 | 20.0 | 4.04 GB | 242 % | 22 | 22.4 |
|
||||
| 25 | Horizon | 50.7 | 89.3 | 19.7 | 2.30 GB | 286 % | 50 | 49.7 |
|
||||
| 25 | Vanilla | 24.2 | 36.5 | 20.0 | 4.40 GB | 320 % | 34 | 33.7 |
|
||||
| 50 | Horizon | 50.7 | 102.7 | 19.7 | 1.72 GB | 213 % | 97 | 96.5 |
|
||||
| 50 | Vanilla | stopped by its watchdog | | | | | | |
|
||||
| 100 | Horizon | 92.9 | 139.6 | 10.8 | 1.70 GB | 322 % | 114 | 113.6 |
|
||||
| 100 | Vanilla | stopped by its watchdog | | | | | | |
|
||||
|
||||
Vanilla's watchdog stopped it at 50 bots (in `ClientboundLightUpdatePacketData`, copying light for a chunk
|
||||
packet) and at 100 (in `Entity.move` from a move packet).
|
||||
|
||||
## Findings
|
||||
|
||||
- Horizon's RSS is 1.9 to 2.6 times smaller in every run, and it starts in 11 s against vanilla's 15 s.
|
||||
Vanilla's heap is fixed at 4 GB (`-Xms4g`), so part of its RSS is that setting rather than need.
|
||||
- With 1 bot Horizon's tick is 3.3 times shorter walking and 4.5 times shorter exploring at equal work;
|
||||
at 10 bots it is still shorter in both.
|
||||
- At equal exploring work Horizon's tick is about half of vanilla's at 10 and 25 bots.
|
||||
- When the bots only fly around spawn (`walk`), vanilla's tick hardly grows with players (16 to 20 ms from
|
||||
1 to 50 bots) while Horizon's grows by about 0.5 ms per bot, so vanilla is ahead from about 20 bots: 16.0
|
||||
against 20.7 ms at 25 and 20.5 against 31.4 ms at 50. Both keep 20 TPS up to 50.
|
||||
- With 100 bots vanilla's watchdog stops it in every pass, and Horizon keeps running at 9 to 11 TPS.
|
||||
|
||||
### Where Horizon's walk tick goes at 25 bots
|
||||
|
||||
Profiled on the ThinkPad with `perf` (the server thread, 20 s, self time):
|
||||
|
||||
| Share | What | Whose |
|
||||
|---:|---|---|
|
||||
| 18 % | `Streaming::update`: every tick it walks all loaded columns and proto chunks and tests each against every player's area (unloading, dropping protos, finishing and decoration candidates), so the cost grows with players times chunks | streaming / engine tick |
|
||||
| 20 % | `PalettedContainer::get` and `World::block`, mostly from block entity ticks: on the Mac profile `tick_hoppers` looks up the block under every block entity in the loaded world each tick and compares names as strings, and spawners rebuild their display id | engine tick |
|
||||
| 8 % | `light_data` when a chunk is sent: the light layers were built twice per chunk; a follow-up commit on #623 builds them once and keeps each section's packed layers | server (this work) |
|
||||
| 7 % | encoding chunk sections and heightmaps for the chunks each bot newly sees | server |
|
||||
| 4 % | `Hub::tick` (moves, tracking) | server |
|
||||
| 2 % | the moved-wrongly collision check | server |
|
||||
|
||||
The first two grow with the number of players but aren't specific to the server: they are the
|
||||
engine's tick and streaming, reported to perf-server-2. Before #623 and #649, encoding every chunk section
|
||||
once per player and waking each connection's writer thread once per packet cost more than everything above
|
||||
(at 50 walking bots the tick was 281 ms; see the earlier runs).
|
||||
|
||||
## Earlier runs (same machine and method)
|
||||
|
||||
- `run-1-before-chunk-cache.json`: before #623, with vanilla's locator bar on. Horizon's walk tick was
|
||||
27.5, 40.7, 281 and 467 ms at 10, 25, 50 and 100 bots, and vanilla's 33.2 ms at 50 (the locator bar).
|
||||
- `run-2-chunk-cache.json`: Horizon with #623 only: 13.2, 23.5, 31.4 and 84.4 ms walking at 10 to 100
|
||||
bots.
|
||||
|
||||
## Files
|
||||
|
||||
- `horizon.json`, `vanilla.json`: the walk and explore passes (vanilla with `locator_bar=false`).
|
||||
- `explore-equal-rate.json`: the explore pass at 0.8 blocks a second, both servers.
|
||||
- `run-1-before-chunk-cache.json`, `run-2-chunk-cache.json`: the earlier runs.
|
||||
|
||||
To run it again (bots on another machine reached at `--connect`):
|
||||
|
||||
cargo build --release -p horizon-client --bin horizon-client -p horizon-server -p horizon-bots
|
||||
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50,100 --behaviour walk,explore \
|
||||
--gamerule locator_bar=false --nice 19 --bots-via 'ssh macmini' --bots-bin /path/to/horizon-bots \
|
||||
--connect <this machine's address> --out results.json
|
||||
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25 --behaviour explore \
|
||||
--explore-speed 0.8 --gamerule locator_bar=false --nice 19 ... --out explore-equal-rate.json
|
||||
@@ -1,267 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T05:47:12+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25",
|
||||
"behaviour": "explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0.8,
|
||||
"gamerule": "locator_bar=false",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/scratch/sh2-bench5.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh -o BatchMode=yes macmini",
|
||||
"bots_bin": "/tmp/sh2-horizon-bots",
|
||||
"connect": "100.72.51.8",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 10.91,
|
||||
"mspt": {
|
||||
"average": 4.02,
|
||||
"p50": 3.75,
|
||||
"p95": 6.63,
|
||||
"p99": 14.55
|
||||
},
|
||||
"mspt_worst_p99": 63.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1392.42,
|
||||
"max": 1746.1
|
||||
},
|
||||
"cpu_percent": 24.52,
|
||||
"chunks_per_second": 1.69,
|
||||
"unique_per_second": 1.34,
|
||||
"forgotten_per_second": 1.62,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 1048,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1048.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 952.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.13,
|
||||
"mspt": {
|
||||
"average": 18.12,
|
||||
"p50": 18.78,
|
||||
"p95": 27.07,
|
||||
"p99": 60.81
|
||||
},
|
||||
"mspt_worst_p99": 1397.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3397.31,
|
||||
"max": 3430.2
|
||||
},
|
||||
"cpu_percent": 72.99,
|
||||
"chunks_per_second": 1.69,
|
||||
"unique_per_second": 1.38,
|
||||
"forgotten_per_second": 1.62,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 1048,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1048.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 952.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 13.49,
|
||||
"p50": 12.09,
|
||||
"p95": 24.81,
|
||||
"p99": 40.9
|
||||
},
|
||||
"mspt_worst_p99": 101.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1684.04,
|
||||
"max": 2660.6
|
||||
},
|
||||
"cpu_percent": 89.04,
|
||||
"chunks_per_second": 13.08,
|
||||
"unique_per_second": 11.84,
|
||||
"forgotten_per_second": 13.02,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 9277,
|
||||
"chunks_per_bot": {
|
||||
"explore": 927.7
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 889.9
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3381,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.84,
|
||||
"mspt": {
|
||||
"average": 29.18,
|
||||
"p50": 26.76,
|
||||
"p95": 43.02,
|
||||
"p99": 80.16
|
||||
},
|
||||
"mspt_worst_p99": 425.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3609.51,
|
||||
"max": 3791.6
|
||||
},
|
||||
"cpu_percent": 142.79,
|
||||
"chunks_per_second": 13.1,
|
||||
"unique_per_second": 11.88,
|
||||
"forgotten_per_second": 13.12,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 9245,
|
||||
"chunks_per_bot": {
|
||||
"explore": 924.5
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 885.3
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3388,
|
||||
"moved_too_quickly": 6,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.02,
|
||||
"mspt": {
|
||||
"average": 20.25,
|
||||
"p50": 18.73,
|
||||
"p95": 36.78,
|
||||
"p99": 55.76
|
||||
},
|
||||
"mspt_worst_p99": 92.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1782.26,
|
||||
"max": 2741.9
|
||||
},
|
||||
"cpu_percent": 120.8,
|
||||
"chunks_per_second": 32.22,
|
||||
"unique_per_second": 28.89,
|
||||
"forgotten_per_second": 32.19,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 23357,
|
||||
"chunks_per_bot": {
|
||||
"explore": 934.28
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 890.32
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 11580,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.54,
|
||||
"mspt": {
|
||||
"average": 37.04,
|
||||
"p50": 34.77,
|
||||
"p95": 49.42,
|
||||
"p99": 101.67
|
||||
},
|
||||
"mspt_worst_p99": 735.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3815.46,
|
||||
"max": 4202.0
|
||||
},
|
||||
"cpu_percent": 169.15,
|
||||
"chunks_per_second": 32.34,
|
||||
"unique_per_second": 28.93,
|
||||
"forgotten_per_second": 32.31,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 23306,
|
||||
"chunks_per_bot": {
|
||||
"explore": 932.24
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 886.96
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 11539,
|
||||
"moved_too_quickly": 6,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T05:04:57+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon",
|
||||
"bots": "1,10,25,50,100",
|
||||
"behaviour": "walk,explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0,
|
||||
"gamerule": "locator_bar=false",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/scratch/sh2-bench4.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh -o BatchMode=yes macmini",
|
||||
"bots_bin": "/tmp/sh2-horizon-bots",
|
||||
"connect": "100.72.51.8",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.02,
|
||||
"mspt": {
|
||||
"average": 5.45,
|
||||
"p50": 4.63,
|
||||
"p95": 12.14,
|
||||
"p99": 30.63
|
||||
},
|
||||
"mspt_worst_p99": 91.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1398.5,
|
||||
"max": 1708.4
|
||||
},
|
||||
"cpu_percent": 57.03,
|
||||
"chunks_per_second": 9.17,
|
||||
"unique_per_second": 0.26,
|
||||
"forgotten_per_second": 9.2,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3708,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3708.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 694.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.02,
|
||||
"mspt": {
|
||||
"average": 11.51,
|
||||
"p50": 10.6,
|
||||
"p95": 22.13,
|
||||
"p99": 36.92
|
||||
},
|
||||
"mspt_worst_p99": 71.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1440.2,
|
||||
"max": 1832.4
|
||||
},
|
||||
"cpu_percent": 124.19,
|
||||
"chunks_per_second": 94.04,
|
||||
"unique_per_second": 2.38,
|
||||
"forgotten_per_second": 93.95,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 37269,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3726.9
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 688.9
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 10.91,
|
||||
"mspt": {
|
||||
"average": 20.74,
|
||||
"p50": 19.27,
|
||||
"p95": 40.42,
|
||||
"p99": 57.12
|
||||
},
|
||||
"mspt_worst_p99": 102.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1485.4,
|
||||
"max": 1871.9
|
||||
},
|
||||
"cpu_percent": 169.52,
|
||||
"chunks_per_second": 235.81,
|
||||
"unique_per_second": 5.68,
|
||||
"forgotten_per_second": 235.83,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 94456,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3778.24
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 692.96
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 31.41,
|
||||
"p50": 25.39,
|
||||
"p95": 78.97,
|
||||
"p99": 110.73
|
||||
},
|
||||
"mspt_worst_p99": 192.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1558.34,
|
||||
"max": 1915.3
|
||||
},
|
||||
"cpu_percent": 173.27,
|
||||
"chunks_per_second": 450.85,
|
||||
"unique_per_second": 10.3,
|
||||
"forgotten_per_second": 451.86,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 186885,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3737.7
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 688.9
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 18.16,
|
||||
"mspt": {
|
||||
"average": 114.55,
|
||||
"p50": 101.37,
|
||||
"p95": 256.19,
|
||||
"p99": 338.64
|
||||
},
|
||||
"mspt_worst_p99": 442.6,
|
||||
"tps": 8.73,
|
||||
"rss_mb": {
|
||||
"mean": 1357.65,
|
||||
"max": 1358.4
|
||||
},
|
||||
"cpu_percent": 181.21,
|
||||
"chunks_per_second": 484.16,
|
||||
"unique_per_second": 30.76,
|
||||
"forgotten_per_second": 478.77,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 212947,
|
||||
"chunks_per_bot": {
|
||||
"walk": 2129.47
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 583.02
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 57
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 17.45,
|
||||
"mspt": {
|
||||
"average": 10.62,
|
||||
"p50": 9.2,
|
||||
"p95": 24.61,
|
||||
"p99": 38.18
|
||||
},
|
||||
"mspt_worst_p99": 81.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1372.73,
|
||||
"max": 1568.7
|
||||
},
|
||||
"cpu_percent": 322.38,
|
||||
"chunks_per_second": 39.21,
|
||||
"unique_per_second": 34.6,
|
||||
"forgotten_per_second": 38.55,
|
||||
"waited_ticks_per_second": 0.51,
|
||||
"chunks_total": 12549,
|
||||
"chunks_per_bot": {
|
||||
"explore": 12549.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 11168.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.32,
|
||||
"mspt": {
|
||||
"average": 30.38,
|
||||
"p50": 27.84,
|
||||
"p95": 52.31,
|
||||
"p99": 77.89
|
||||
},
|
||||
"mspt_worst_p99": 158.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 2231.56,
|
||||
"max": 4022.6
|
||||
},
|
||||
"cpu_percent": 344.99,
|
||||
"chunks_per_second": 47.34,
|
||||
"unique_per_second": 46.73,
|
||||
"forgotten_per_second": 44.57,
|
||||
"waited_ticks_per_second": 112.27,
|
||||
"chunks_total": 19870,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1987.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1966.5
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 16130,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.42,
|
||||
"mspt": {
|
||||
"average": 50.67,
|
||||
"p50": 47.61,
|
||||
"p95": 89.3,
|
||||
"p99": 119.15
|
||||
},
|
||||
"mspt_worst_p99": 222.3,
|
||||
"tps": 19.73,
|
||||
"rss_mb": {
|
||||
"mean": 2358.4,
|
||||
"max": 4273.7
|
||||
},
|
||||
"cpu_percent": 286.32,
|
||||
"chunks_per_second": 50.39,
|
||||
"unique_per_second": 49.67,
|
||||
"forgotten_per_second": 47.45,
|
||||
"waited_ticks_per_second": 311.85,
|
||||
"chunks_total": 28954,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1158.16
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1147.56
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 36319,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 20.06,
|
||||
"mspt": {
|
||||
"average": 50.69,
|
||||
"p50": 47.48,
|
||||
"p95": 102.67,
|
||||
"p99": 133.27
|
||||
},
|
||||
"mspt_worst_p99": 274.4,
|
||||
"tps": 19.73,
|
||||
"rss_mb": {
|
||||
"mean": 1764.01,
|
||||
"max": 2542.0
|
||||
},
|
||||
"cpu_percent": 212.83,
|
||||
"chunks_per_second": 97.33,
|
||||
"unique_per_second": 96.47,
|
||||
"forgotten_per_second": 76.74,
|
||||
"waited_ticks_per_second": 750.49,
|
||||
"chunks_total": 38538,
|
||||
"chunks_per_bot": {
|
||||
"explore": 770.76
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 763.84
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 39880,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.82,
|
||||
"mspt": {
|
||||
"average": 92.93,
|
||||
"p50": 87.84,
|
||||
"p95": 139.6,
|
||||
"p99": 178.38
|
||||
},
|
||||
"mspt_worst_p99": 272.9,
|
||||
"tps": 10.76,
|
||||
"rss_mb": {
|
||||
"mean": 1736.38,
|
||||
"max": 1855.7
|
||||
},
|
||||
"cpu_percent": 321.53,
|
||||
"chunks_per_second": 113.89,
|
||||
"unique_per_second": 113.56,
|
||||
"forgotten_per_second": 101.68,
|
||||
"waited_ticks_per_second": 1179.4,
|
||||
"chunks_total": 84617,
|
||||
"chunks_per_bot": {
|
||||
"explore": 846.17
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 845.01
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 128104,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,693 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T00:37:35+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50,100",
|
||||
"behaviour": "walk,explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/scratch/sh2-bench.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh -o BatchMode=yes macmini",
|
||||
"bots_bin": "/tmp/sh2-horizon-bots",
|
||||
"connect": "100.72.51.8",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 7.63,
|
||||
"p50": 5.51,
|
||||
"p95": 17.46,
|
||||
"p99": 36.79
|
||||
},
|
||||
"mspt_worst_p99": 79.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1391.94,
|
||||
"max": 1700.7
|
||||
},
|
||||
"cpu_percent": 60.18,
|
||||
"chunks_per_second": 9.15,
|
||||
"unique_per_second": 0.29,
|
||||
"forgotten_per_second": 9.18,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3682,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3682.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 692.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.35,
|
||||
"mspt": {
|
||||
"average": 18.08,
|
||||
"p50": 18.66,
|
||||
"p95": 28.05,
|
||||
"p99": 46.94
|
||||
},
|
||||
"mspt_worst_p99": 456.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3455.72,
|
||||
"max": 3471.7
|
||||
},
|
||||
"cpu_percent": 65.67,
|
||||
"chunks_per_second": 9.89,
|
||||
"unique_per_second": 0.29,
|
||||
"forgotten_per_second": 9.92,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3827,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3827.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.22,
|
||||
"mspt": {
|
||||
"average": 27.53,
|
||||
"p50": 24.93,
|
||||
"p95": 53.93,
|
||||
"p99": 77.03
|
||||
},
|
||||
"mspt_worst_p99": 111.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1433.85,
|
||||
"max": 1831.9
|
||||
},
|
||||
"cpu_percent": 138.65,
|
||||
"chunks_per_second": 95.98,
|
||||
"unique_per_second": 2.38,
|
||||
"forgotten_per_second": 95.78,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 38041,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3804.1
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 690.8
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.83,
|
||||
"mspt": {
|
||||
"average": 18.92,
|
||||
"p50": 16.02,
|
||||
"p95": 39.0,
|
||||
"p99": 56.13
|
||||
},
|
||||
"mspt_worst_p99": 180.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3479.45,
|
||||
"max": 3535.7
|
||||
},
|
||||
"cpu_percent": 99.39,
|
||||
"chunks_per_second": 99.93,
|
||||
"unique_per_second": 2.94,
|
||||
"forgotten_per_second": 100.04,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 38411,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3841.1
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 697.1
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 7,
|
||||
"moved_too_quickly": 7,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.52,
|
||||
"mspt": {
|
||||
"average": 40.69,
|
||||
"p50": 37.28,
|
||||
"p95": 81.16,
|
||||
"p99": 108.42
|
||||
},
|
||||
"mspt_worst_p99": 149.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1444.12,
|
||||
"max": 1882.6
|
||||
},
|
||||
"cpu_percent": 176.94,
|
||||
"chunks_per_second": 244.11,
|
||||
"unique_per_second": 6.51,
|
||||
"forgotten_per_second": 243.96,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 95219,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3808.76
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 695.64
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.23,
|
||||
"mspt": {
|
||||
"average": 18.81,
|
||||
"p50": 17.26,
|
||||
"p95": 30.16,
|
||||
"p99": 53.9
|
||||
},
|
||||
"mspt_worst_p99": 390.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3517.36,
|
||||
"max": 3555.7
|
||||
},
|
||||
"cpu_percent": 148.52,
|
||||
"chunks_per_second": 252.34,
|
||||
"unique_per_second": 7.75,
|
||||
"forgotten_per_second": 251.95,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 96645,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3865.8
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 698.4
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 11,
|
||||
"moved_too_quickly": 11,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 12.72,
|
||||
"mspt": {
|
||||
"average": 281.42,
|
||||
"p50": 278.28,
|
||||
"p95": 343.64,
|
||||
"p99": 384.23
|
||||
},
|
||||
"mspt_worst_p99": 475.1,
|
||||
"tps": 3.55,
|
||||
"rss_mb": {
|
||||
"mean": 1334.32,
|
||||
"max": 1335.4
|
||||
},
|
||||
"cpu_percent": 197.11,
|
||||
"chunks_per_second": 391.5,
|
||||
"unique_per_second": 21.24,
|
||||
"forgotten_per_second": 388.33,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 153136,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3062.72
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 661.82
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 57
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.74,
|
||||
"mspt": {
|
||||
"average": 33.21,
|
||||
"p50": 29.9,
|
||||
"p95": 53.37,
|
||||
"p99": 109.14
|
||||
},
|
||||
"mspt_worst_p99": 833.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3568.07,
|
||||
"max": 3594.9
|
||||
},
|
||||
"cpu_percent": 266.59,
|
||||
"chunks_per_second": 496.3,
|
||||
"unique_per_second": 18.64,
|
||||
"forgotten_per_second": 495.77,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 191965,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3839.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 696.22
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 184,
|
||||
"moved_too_quickly": 183,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.22,
|
||||
"mspt": {
|
||||
"average": 467.37,
|
||||
"p50": 464.62,
|
||||
"p95": 524.07,
|
||||
"p99": 548.77
|
||||
},
|
||||
"mspt_worst_p99": 581.1,
|
||||
"tps": 2.14,
|
||||
"rss_mb": {
|
||||
"mean": 1353.68,
|
||||
"max": 1354.9
|
||||
},
|
||||
"cpu_percent": 191.96,
|
||||
"chunks_per_second": 377.62,
|
||||
"unique_per_second": 55.8,
|
||||
"forgotten_per_second": 348.88,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 165719,
|
||||
"chunks_per_bot": {
|
||||
"walk": 1657.19
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 549.8
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 56
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.84,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 17.07,
|
||||
"p50": 16.6,
|
||||
"p95": 32.82,
|
||||
"p99": 50.3
|
||||
},
|
||||
"mspt_worst_p99": 113.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1391.72,
|
||||
"max": 1566.6
|
||||
},
|
||||
"cpu_percent": 333.8,
|
||||
"chunks_per_second": 39.42,
|
||||
"unique_per_second": 34.77,
|
||||
"forgotten_per_second": 39.34,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 13860,
|
||||
"chunks_per_bot": {
|
||||
"explore": 13860.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 12379.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 192,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 16.03,
|
||||
"mspt": {
|
||||
"average": 9.9,
|
||||
"p50": 8.04,
|
||||
"p95": 20.41,
|
||||
"p99": 63.62
|
||||
},
|
||||
"mspt_worst_p99": 351.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3709.92,
|
||||
"max": 3776.2
|
||||
},
|
||||
"cpu_percent": 246.64,
|
||||
"chunks_per_second": 36.06,
|
||||
"unique_per_second": 33.33,
|
||||
"forgotten_per_second": 35.73,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 12369,
|
||||
"chunks_per_bot": {
|
||||
"explore": 12369.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 11559.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 222,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.42,
|
||||
"mspt": {
|
||||
"average": 33.72,
|
||||
"p50": 31.08,
|
||||
"p95": 61.07,
|
||||
"p99": 89.09
|
||||
},
|
||||
"mspt_worst_p99": 142.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 2239.97,
|
||||
"max": 4108.8
|
||||
},
|
||||
"cpu_percent": 350.83,
|
||||
"chunks_per_second": 46.49,
|
||||
"unique_per_second": 46.08,
|
||||
"forgotten_per_second": 44.33,
|
||||
"waited_ticks_per_second": 111.28,
|
||||
"chunks_total": 19915,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1991.5
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1974.4
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 17516,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.15,
|
||||
"mspt": {
|
||||
"average": 16.25,
|
||||
"p50": 13.67,
|
||||
"p95": 28.92,
|
||||
"p99": 115.9
|
||||
},
|
||||
"mspt_worst_p99": 1202.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4202.72,
|
||||
"max": 4502.7
|
||||
},
|
||||
"cpu_percent": 247.47,
|
||||
"chunks_per_second": 22.3,
|
||||
"unique_per_second": 22.25,
|
||||
"forgotten_per_second": 22.64,
|
||||
"waited_ticks_per_second": 78.89,
|
||||
"chunks_total": 10092,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1009.2
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1007.4
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 16286,
|
||||
"moved_too_quickly": 12,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.82,
|
||||
"mspt": {
|
||||
"average": 48.03,
|
||||
"p50": 44.99,
|
||||
"p95": 83.06,
|
||||
"p99": 109.63
|
||||
},
|
||||
"mspt_worst_p99": 191.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 2214.16,
|
||||
"max": 4598.6
|
||||
},
|
||||
"cpu_percent": 356.03,
|
||||
"chunks_per_second": 64.2,
|
||||
"unique_per_second": 63.5,
|
||||
"forgotten_per_second": 60.9,
|
||||
"waited_ticks_per_second": 301.65,
|
||||
"chunks_total": 32247,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1289.88
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1279.4
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 42159,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.73,
|
||||
"mspt": {
|
||||
"average": 25.73,
|
||||
"p50": 22.75,
|
||||
"p95": 37.72,
|
||||
"p99": 201.98
|
||||
},
|
||||
"mspt_worst_p99": 4051.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4355.9,
|
||||
"max": 4983.2
|
||||
},
|
||||
"cpu_percent": 309.3,
|
||||
"chunks_per_second": 33.33,
|
||||
"unique_per_second": 33.21,
|
||||
"forgotten_per_second": 34.22,
|
||||
"waited_ticks_per_second": 258.68,
|
||||
"chunks_total": 19426,
|
||||
"chunks_per_bot": {
|
||||
"explore": 777.04
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 772.92
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 53918,
|
||||
"moved_too_quickly": 93,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.01,
|
||||
"mspt": {
|
||||
"average": 108.61,
|
||||
"p50": 107.37,
|
||||
"p95": 151.96,
|
||||
"p99": 177.41
|
||||
},
|
||||
"mspt_worst_p99": 271.6,
|
||||
"tps": 9.21,
|
||||
"rss_mb": {
|
||||
"mean": 1775.98,
|
||||
"max": 1956.9
|
||||
},
|
||||
"cpu_percent": 359.7,
|
||||
"chunks_per_second": 100.32,
|
||||
"unique_per_second": 98.91,
|
||||
"forgotten_per_second": 95.42,
|
||||
"waited_ticks_per_second": 518.15,
|
||||
"chunks_total": 55956,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1119.12
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1111.42
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 69967,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.74,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 244.43,
|
||||
"p50": 243.58,
|
||||
"p95": 308.39,
|
||||
"p99": 360.12
|
||||
},
|
||||
"mspt_worst_p99": 470.2,
|
||||
"tps": 4.09,
|
||||
"rss_mb": {
|
||||
"mean": 1806.85,
|
||||
"max": 2084.5
|
||||
},
|
||||
"cpu_percent": 310.46,
|
||||
"chunks_per_second": 186.87,
|
||||
"unique_per_second": 182.99,
|
||||
"forgotten_per_second": 161.75,
|
||||
"waited_ticks_per_second": 1131.65,
|
||||
"chunks_total": 100389,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1003.89
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 991.9
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 56791,
|
||||
"moved_too_quickly": 344,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 57
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.83,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T02:22:59+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon",
|
||||
"bots": "1,10,25,50,100",
|
||||
"behaviour": "walk,explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/scratch/sh2-bench2.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh -o BatchMode=yes macmini",
|
||||
"bots_bin": "/tmp/sh2-horizon-bots",
|
||||
"connect": "100.72.51.8",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 5.41,
|
||||
"p50": 4.63,
|
||||
"p95": 11.61,
|
||||
"p99": 30.5
|
||||
},
|
||||
"mspt_worst_p99": 64.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1398.84,
|
||||
"max": 1708.5
|
||||
},
|
||||
"cpu_percent": 56.84,
|
||||
"chunks_per_second": 9.19,
|
||||
"unique_per_second": 0.27,
|
||||
"forgotten_per_second": 9.22,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3713,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3713.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 694.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 10.92,
|
||||
"mspt": {
|
||||
"average": 13.18,
|
||||
"p50": 11.74,
|
||||
"p95": 26.5,
|
||||
"p99": 44.82
|
||||
},
|
||||
"mspt_worst_p99": 87.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1439.83,
|
||||
"max": 1831.6
|
||||
},
|
||||
"cpu_percent": 132.94,
|
||||
"chunks_per_second": 93.87,
|
||||
"unique_per_second": 2.38,
|
||||
"forgotten_per_second": 93.77,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 37240,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3724.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 688.7
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 10.92,
|
||||
"mspt": {
|
||||
"average": 23.52,
|
||||
"p50": 21.92,
|
||||
"p95": 44.7,
|
||||
"p99": 63.12
|
||||
},
|
||||
"mspt_worst_p99": 119.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1485.82,
|
||||
"max": 1872.0
|
||||
},
|
||||
"cpu_percent": 173.82,
|
||||
"chunks_per_second": 235.63,
|
||||
"unique_per_second": 5.64,
|
||||
"forgotten_per_second": 235.72,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 94549,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3781.96
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 692.8
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 10.91,
|
||||
"mspt": {
|
||||
"average": 31.36,
|
||||
"p50": 26.08,
|
||||
"p95": 70.6,
|
||||
"p99": 92.83
|
||||
},
|
||||
"mspt_worst_p99": 125.5,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1556.81,
|
||||
"max": 1913.0
|
||||
},
|
||||
"cpu_percent": 192.16,
|
||||
"chunks_per_second": 468.69,
|
||||
"unique_per_second": 10.43,
|
||||
"forgotten_per_second": 468.78,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 193547,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3870.94
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 692.84
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 11.02,
|
||||
"mspt": {
|
||||
"average": 84.38,
|
||||
"p50": 81.5,
|
||||
"p95": 132.92,
|
||||
"p99": 168.38
|
||||
},
|
||||
"mspt_worst_p99": 245.5,
|
||||
"tps": 11.85,
|
||||
"rss_mb": {
|
||||
"mean": 1395.64,
|
||||
"max": 1941.3
|
||||
},
|
||||
"cpu_percent": 271.28,
|
||||
"chunks_per_second": 850.05,
|
||||
"unique_per_second": 21.79,
|
||||
"forgotten_per_second": 851.1,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 368464,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3684.64
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 680.58
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.02,
|
||||
"mspt": {
|
||||
"average": 11.63,
|
||||
"p50": 10.27,
|
||||
"p95": 24.52,
|
||||
"p99": 40.53
|
||||
},
|
||||
"mspt_worst_p99": 99.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1413.81,
|
||||
"max": 1596.1
|
||||
},
|
||||
"cpu_percent": 329.84,
|
||||
"chunks_per_second": 39.47,
|
||||
"unique_per_second": 34.81,
|
||||
"forgotten_per_second": 39.42,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 13798,
|
||||
"chunks_per_bot": {
|
||||
"explore": 13798.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 12330.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 185,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.02,
|
||||
"mspt": {
|
||||
"average": 28.49,
|
||||
"p50": 26.2,
|
||||
"p95": 48.58,
|
||||
"p99": 72.04
|
||||
},
|
||||
"mspt_worst_p99": 125.7,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 2231.06,
|
||||
"max": 4077.8
|
||||
},
|
||||
"cpu_percent": 344.95,
|
||||
"chunks_per_second": 46.31,
|
||||
"unique_per_second": 45.88,
|
||||
"forgotten_per_second": 44.14,
|
||||
"waited_ticks_per_second": 111.95,
|
||||
"chunks_total": 19722,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1972.2
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1957.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 18533,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.12,
|
||||
"mspt": {
|
||||
"average": 40.04,
|
||||
"p50": 37.61,
|
||||
"p95": 62.39,
|
||||
"p99": 83.64
|
||||
},
|
||||
"mspt_worst_p99": 147.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 2466.85,
|
||||
"max": 4504.5
|
||||
},
|
||||
"cpu_percent": 340.13,
|
||||
"chunks_per_second": 60.61,
|
||||
"unique_per_second": 59.62,
|
||||
"forgotten_per_second": 56.28,
|
||||
"waited_ticks_per_second": 299.57,
|
||||
"chunks_total": 32877,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1315.08
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1300.72
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 46272,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 11.32,
|
||||
"mspt": {
|
||||
"average": 61.82,
|
||||
"p50": 59.21,
|
||||
"p95": 90.46,
|
||||
"p99": 115.58
|
||||
},
|
||||
"mspt_worst_p99": 320.9,
|
||||
"tps": 16.18,
|
||||
"rss_mb": {
|
||||
"mean": 2634.54,
|
||||
"max": 4883.1
|
||||
},
|
||||
"cpu_percent": 356.96,
|
||||
"chunks_per_second": 95.26,
|
||||
"unique_per_second": 94.31,
|
||||
"forgotten_per_second": 85.86,
|
||||
"waited_ticks_per_second": 528.08,
|
||||
"chunks_total": 59251,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1185.02
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1173.96
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 99493,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 10.81,
|
||||
"mspt": {
|
||||
"average": 103.24,
|
||||
"p50": 100.03,
|
||||
"p95": 144.77,
|
||||
"p99": 178.8
|
||||
},
|
||||
"mspt_worst_p99": 258.6,
|
||||
"tps": 9.69,
|
||||
"rss_mb": {
|
||||
"mean": 1784.48,
|
||||
"max": 1922.0
|
||||
},
|
||||
"cpu_percent": 336.94,
|
||||
"chunks_per_second": 115.06,
|
||||
"unique_per_second": 114.86,
|
||||
"forgotten_per_second": 109.43,
|
||||
"waited_ticks_per_second": 1127.57,
|
||||
"chunks_total": 93839,
|
||||
"chunks_per_bot": {
|
||||
"explore": 938.39
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 935.28
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 133404,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 100,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-17T03:29:30+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "vanilla",
|
||||
"bots": "1,10,25,50,100",
|
||||
"behaviour": "walk,explore",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"gamerule": "locator_bar=false",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/scratch/sh2-bench3.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh -o BatchMode=yes macmini",
|
||||
"bots_bin": "/tmp/sh2-horizon-bots",
|
||||
"connect": "100.72.51.8",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.43,
|
||||
"mspt": {
|
||||
"average": 18.32,
|
||||
"p50": 19.26,
|
||||
"p95": 27.34,
|
||||
"p99": 45.48
|
||||
},
|
||||
"mspt_worst_p99": 393.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3428.43,
|
||||
"max": 3437.7
|
||||
},
|
||||
"cpu_percent": 66.47,
|
||||
"chunks_per_second": 9.92,
|
||||
"unique_per_second": 0.29,
|
||||
"forgotten_per_second": 9.95,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3850,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3850.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.76,
|
||||
"mspt": {
|
||||
"average": 16.8,
|
||||
"p50": 14.55,
|
||||
"p95": 30.52,
|
||||
"p99": 50.01
|
||||
},
|
||||
"mspt_worst_p99": 205.2,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3490.15,
|
||||
"max": 3580.7
|
||||
},
|
||||
"cpu_percent": 83.7,
|
||||
"chunks_per_second": 100.01,
|
||||
"unique_per_second": 2.83,
|
||||
"forgotten_per_second": 100.04,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 38623,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3862.3
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 697.2
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 5,
|
||||
"moved_too_quickly": 5,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.84,
|
||||
"mspt": {
|
||||
"average": 16.01,
|
||||
"p50": 14.55,
|
||||
"p95": 26.39,
|
||||
"p99": 51.26
|
||||
},
|
||||
"mspt_worst_p99": 397.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3550.02,
|
||||
"max": 3593.5
|
||||
},
|
||||
"cpu_percent": 114.96,
|
||||
"chunks_per_second": 251.62,
|
||||
"unique_per_second": 6.72,
|
||||
"forgotten_per_second": 251.16,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 98701,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3948.04
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 698.84
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 10,
|
||||
"moved_too_quickly": 10,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 14.43,
|
||||
"mspt": {
|
||||
"average": 20.52,
|
||||
"p50": 19.55,
|
||||
"p95": 29.72,
|
||||
"p99": 49.04
|
||||
},
|
||||
"mspt_worst_p99": 600.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3633.12,
|
||||
"max": 3658.7
|
||||
},
|
||||
"cpu_percent": 193.84,
|
||||
"chunks_per_second": 501.86,
|
||||
"unique_per_second": 12.15,
|
||||
"forgotten_per_second": 501.8,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 203429,
|
||||
"chunks_per_bot": {
|
||||
"walk": 4068.58
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 698.66
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 33,
|
||||
"moved_too_quickly": 32,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "walk",
|
||||
"start_seconds": 15.34,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.55,
|
||||
"mspt": {
|
||||
"average": 9.93,
|
||||
"p50": 8.12,
|
||||
"p95": 21.33,
|
||||
"p99": 60.19
|
||||
},
|
||||
"mspt_worst_p99": 373.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3564.11,
|
||||
"max": 3655.0
|
||||
},
|
||||
"cpu_percent": 245.18,
|
||||
"chunks_per_second": 35.93,
|
||||
"unique_per_second": 33.35,
|
||||
"forgotten_per_second": 35.64,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 12340,
|
||||
"chunks_per_bot": {
|
||||
"explore": 12340.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 11569.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 261,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 15.03,
|
||||
"mspt": {
|
||||
"average": 16.99,
|
||||
"p50": 14.46,
|
||||
"p95": 29.61,
|
||||
"p99": 132.18
|
||||
},
|
||||
"mspt_worst_p99": 1244.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4135.2,
|
||||
"max": 4453.1
|
||||
},
|
||||
"cpu_percent": 242.32,
|
||||
"chunks_per_second": 22.39,
|
||||
"unique_per_second": 22.41,
|
||||
"forgotten_per_second": 22.78,
|
||||
"waited_ticks_per_second": 78.78,
|
||||
"chunks_total": 10177,
|
||||
"chunks_per_bot": {
|
||||
"explore": 1017.7
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 1016.2
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 17314,
|
||||
"moved_too_quickly": 19,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.84,
|
||||
"mspt": {
|
||||
"average": 24.19,
|
||||
"p50": 21.22,
|
||||
"p95": 36.51,
|
||||
"p99": 174.82
|
||||
},
|
||||
"mspt_worst_p99": 1532.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4504.55,
|
||||
"max": 4988.9
|
||||
},
|
||||
"cpu_percent": 319.55,
|
||||
"chunks_per_second": 33.78,
|
||||
"unique_per_second": 33.66,
|
||||
"forgotten_per_second": 34.64,
|
||||
"waited_ticks_per_second": 269.22,
|
||||
"chunks_total": 20522,
|
||||
"chunks_per_bot": {
|
||||
"explore": 820.88
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"explore": 815.76
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 46246,
|
||||
"moved_too_quickly": 28,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.73,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 100,
|
||||
"behaviour": "explore",
|
||||
"start_seconds": 14.54,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
# Horizon's server against vanilla 26.3, Paper and Fabric + Lithium: mixed bots, equal work (2026-10-19)
|
||||
|
||||
This is the same load as `../server-equal-mixed-2026-10-18/`, now with two more servers: Paper, and vanilla on
|
||||
Fabric with Lithium. It also uses a newer Horizon release (v2026.10.19). That release spawns mobs around every
|
||||
player (#950) and ticks blocks around every player (#931), as vanilla does. The 2026-10-18 build did neither for
|
||||
players away from spawn, so it had less work to do than vanilla, and its tick times there were lower than
|
||||
they would have been with equal work. These numbers replace them.
|
||||
|
||||
## Summary
|
||||
|
||||
- **The work was equal** at every count, on all four servers. Chunks sent a second and first-time chunks a
|
||||
second were within 4 % of each other, and the walking bots got about the same chunks each (6050 to 6510).
|
||||
Every bot joined and none was dropped. Vanilla's 50-bot server is the exception: its watchdog stopped it.
|
||||
- **Memory:** Horizon used 0.72 GB with one player and 1.11 GB with 50. The three Java servers used 3.4 to
|
||||
4.5 GB (a 4 GB heap each), so Horizon was **3.8 to 4.8 times smaller** at every count.
|
||||
- **Start:** **8.2 to 8.5 s** to `Done` for Horizon, against 14.7 to 15.5 s for vanilla, 16.6 to 17.6 s for
|
||||
Fabric + Lithium and 19.0 to 22.4 s for Paper.
|
||||
- **CPU** (100 % is one of the machine's 4 threads): from 10 players up, Horizon used the least: 143 % at 50,
|
||||
against Paper's 210 % and Fabric's 236 %. With one player Paper used less (32 % against 46 %).
|
||||
- **Average tick (MSPT), where Horizon loses:**
|
||||
- With 1 player, Paper (10.3 ms) and Fabric + Lithium (12.5) were faster than Horizon (13.7). Vanilla was
|
||||
slowest (17.3).
|
||||
- With 10, Fabric (20.2) and Paper (20.6) were ahead of Horizon (22.9), which tied vanilla (22.9).
|
||||
- With 25, Paper (28.3) was ahead of Horizon (30.9), and Horizon was ahead of Fabric (31.4) and vanilla (37.4).
|
||||
- With 50, Paper (46.9) and Horizon (47.8) were close. Fabric + Lithium fell to 17.2 TPS (58.3 ms), and
|
||||
vanilla's watchdog stopped its server.
|
||||
- **Slow ticks, where Horizon wins:** Horizon's P99 was the lowest from 10 players up (72.0 ms at 25 and 73.2
|
||||
at 50, against Paper's 90.4 and 109.7). Its worst 5-second P99 was the lowest at every count (133 ms at
|
||||
50, against 213 for Paper and 888 for Fabric).
|
||||
|
||||
So on this 4-thread laptop CPU, Horizon is not yet the fastest on average tick time. Paper is ahead at every
|
||||
count: Horizon's average tick is a third longer than Paper's with one player and 2 % longer with 50. Fabric +
|
||||
Lithium is ahead with 1 and 10 players.
|
||||
Horizon is ahead on memory, start time, CPU from 10 players up, and the steadiness of its ticks (P99). Its
|
||||
average tick is what perf-server-11 is working on.
|
||||
|
||||
## Results
|
||||
|
||||
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and P99
|
||||
are means over the 5-second queries of the 300 measured seconds. "Worst P99" is the highest P99 of a single
|
||||
query. RSS and CPU cover the server's process tree. "First-time chunks/s" counts chunks a bot hadn't
|
||||
received before (summed over the bots). "Pulled back" counts position packets that moved a bot back.
|
||||
|
||||
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | First-time chunks/s | Pulled back |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | 13.70 | 19.06 | 27.38 | **43.4** | 20 | **0.72 / 0.72 GB** | 46 % | **8.3 s** | 9.94 | 0.01 | 0 |
|
||||
| 1 | vanilla | 17.31 | 24.68 | 36.43 | 71.0 | 20 | 3.44 / 3.44 GB | 55 % | 14.9 s | 9.99 | 0.00 | 0 |
|
||||
| 1 | Paper | **10.31** | 13.90 | 22.50 | 81.2 | 20 | 4.24 / 4.25 GB | **32 %** | 22.3 s | 9.81 | 0.02 | 0 |
|
||||
| 1 | Fabric + Lithium | 12.46 | 17.69 | 32.74 | 70.1 | 20 | 3.43 / 3.43 GB | 46 % | 17.6 s | 9.98 | 0.00 | 0 |
|
||||
| 10 | Horizon | 22.93 | 39.83 | 53.33 | **89.5** | 20 | **0.92 / 0.97 GB** | **67 %** | **8.2 s** | 61.21 | 1.78 | 3702 |
|
||||
| 10 | vanilla | 22.94 | 36.92 | 56.48 | 196.6 | 20 | 3.71 / 3.76 GB | 101 % | 14.7 s | 61.63 | 1.84 | 3370 |
|
||||
| 10 | Paper | 20.64 | 35.94 | 56.29 | 166.9 | 20 | 4.07 / 4.08 GB | 74 % | 20.5 s | 60.76 | 1.85 | 3974 |
|
||||
| 10 | Fabric + Lithium | **20.18** | 37.04 | 56.08 | 217.9 | 20 | 3.73 / 3.78 GB | 94 % | 16.6 s | 61.65 | 1.84 | 4002 |
|
||||
| 25 | Horizon | 30.92 | 48.25 | **72.00** | **121.1** | 20 | **1.01 / 1.12 GB** | **99 %** | **8.5 s** | 155.81 | 5.78 | 3008 |
|
||||
| 25 | vanilla | 37.35 | 48.20 | 82.66 | 579.8 | 20 | 3.94 / 4.16 GB | 191 % | 15.5 s | 158.52 | 5.71 | 3762 |
|
||||
| 25 | Paper | **28.30** | 61.68 | 90.40 | 124.3 | 20 | 4.16 / 4.25 GB | 124 % | 19.0 s | 156.35 | 5.72 | 4337 |
|
||||
| 25 | Fabric + Lithium | 31.44 | 52.45 | 93.35 | 409.3 | 20 | 3.88 / 4.19 GB | 174 % | 17.5 s | 158.89 | 5.75 | 4194 |
|
||||
| 50 | Horizon | 47.83 | 62.42 | **73.21** | **133.0** | 20 | **1.11 / 1.30 GB** | **143 %** | **8.3 s** | 302.68 | 11.02 | 8562 |
|
||||
| 50 | vanilla | stopped by its watchdog | | | | | | | | | | |
|
||||
| 50 | Paper | **46.86** | 61.16 | 109.72 | 213.4 | 20 | 4.35 / 4.52 GB | 210 % | 19.4 s | 310.51 | 11.16 | 7344 |
|
||||
| 50 | Fabric + Lithium | 58.30 | 83.06 | 174.37 | 887.6 | 17.15 | 4.52 / 4.74 GB | 236 % | 17.5 s | 311.52 | 11.28 | 6350 |
|
||||
|
||||
Vanilla at 50: all 50 bots joined by 05:22:28 UTC. The server fell further and further behind ("Can't keep up!
|
||||
… Running 16229ms or 324 ticks behind"), and at 05:28:17, during the warm-up, its watchdog stopped it: "A
|
||||
single server tick took 60.00 seconds". That is the same as on 2026-10-18.
|
||||
|
||||
Chunks each bot received over the whole run, by behaviour (mean):
|
||||
|
||||
| Bots | Server | walk | explore | idle |
|
||||
|---:|---|---:|---:|---:|
|
||||
| 1 | Horizon | 6280 | | |
|
||||
| 1 | vanilla | 6210 | | |
|
||||
| 1 | Paper | 6049 | | |
|
||||
| 1 | Fabric + Lithium | 6188 | | |
|
||||
| 10 | Horizon | 6460 | 1195 | 473 |
|
||||
| 10 | vanilla | 6272 | 1180 | 473 |
|
||||
| 10 | Paper | 6177 | 1171 | 473 |
|
||||
| 10 | Fabric + Lithium | 6291 | 1181 | 473 |
|
||||
| 25 | Horizon | 6506 | 1262 | 473 |
|
||||
| 25 | vanilla | 6307 | 1236 | 473 |
|
||||
| 25 | Paper | 6280 | 1229 | 473 |
|
||||
| 25 | Fabric + Lithium | 6357 | 1239 | 473 |
|
||||
| 50 | Horizon | 6468 | 1251 | 473 |
|
||||
| 50 | Paper | 6411 | 1224 | 473 |
|
||||
| 50 | Fabric + Lithium | 6332 | 1228 | 473 |
|
||||
|
||||
## Equal work
|
||||
|
||||
`server_bench.py`'s work table compares, at each bot count, every server's chunks sent a second and
|
||||
first-time chunks a second with the busiest server's. The work is equal when both are within 10 % (or within
|
||||
a chunk a second when there are few).
|
||||
|
||||
| Bots | Chunks/s sent, first-time (Horizon; vanilla; Paper; Fabric + Lithium) | Equal |
|
||||
|---:|---|---|
|
||||
| 1 | 9.94, 0.01; 9.99, 0.00; 9.81, 0.02; 9.98, 0.00 | yes |
|
||||
| 10 | 61.21, 1.78; 61.63, 1.84; 60.76, 1.85; 61.65, 1.84 | yes |
|
||||
| 25 | 155.81, 5.78; 158.52, 5.71; 156.35, 5.72; 158.89, 5.75 | yes |
|
||||
| 50 | 302.68, 11.02; (stopped); 310.51, 11.16; 311.52, 11.28 | yes |
|
||||
|
||||
Horizon pulled no bot back for moving too quickly (`moved too quickly`: 0 at every count). The Java servers
|
||||
logged 0 to 46 such lines a run. All four sent position corrections ("Pulled back") at similar rates.
|
||||
|
||||
## Method
|
||||
|
||||
`tools/server_bench.py` ran each configuration on a fresh server:
|
||||
|
||||
- the same `server.properties` on all four: seed 12345, view and simulation distance 10, offline, creative,
|
||||
difficulty easy, `sync-chunk-writes=true`, white-list off;
|
||||
- `gamerule respawn_radius 0`, and otherwise each server's default game rules;
|
||||
- the server waits for `Done`, the bots join one a second, warm up for 300 seconds and are measured for 300,
|
||||
with `/tick query` every 5 seconds and RSS and CPU every second.
|
||||
|
||||
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice deterministic
|
||||
per bot. All are creative and fly 24 blocks above spawn. `walk` flies to random points within 48 blocks at
|
||||
5.6 blocks a second, `explore` flies straight out at 0.8 blocks a second (`--explore-speed 0.8`), and `idle`
|
||||
hovers.
|
||||
|
||||
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS. The desktop was meant to
|
||||
run it but didn't wake (`~/fleet/power wake desktop` timed out twice). No other session ran on the ThinkPad
|
||||
(`load.txt`, a sample a minute: 0 other sessions throughout). The benchmark ran under `nice -n 19` and
|
||||
`ionice -c3`. The bots ran on the Mac mini (M4 Pro, shared with other work at 51 to 98 % CPU, 82 % on
|
||||
average) and reached the ThinkPad over the tailnet.
|
||||
|
||||
**What ran.**
|
||||
|
||||
- Horizon: release v2026.10.19 (07f2913d), the server archive `horizon-server-linux-x86_64.tar.gz` installed
|
||||
with `install.sh`. `horizon-server` ran its `horizon-engine` with game files prepared from the owner's own
|
||||
Minecraft.
|
||||
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
|
||||
- Paper: 26.3 build 205 (`26.3-205-main@a90108f`), from PaperMC's download API, checked against its SHA-256.
|
||||
It ran with Paper's defaults except `connection-throttle: -1` in `bukkit.yml`. Every bot connects from one
|
||||
address, and the default throttle (4 s) refused all but the first. Paperclip patched the pinned server jar
|
||||
from its cache. Same Java and heap.
|
||||
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`tools/benchmark-deps/fabric.lock.json`) with Lithium
|
||||
`mc26.3-0.26.2-fabric` (`tools/benchmark-deps/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
|
||||
Same Java and heap.
|
||||
|
||||
The Paper and Fabric jars are dev-only downloads into `.local/server-bench/jars` and are never committed or
|
||||
shipped.
|
||||
|
||||
**How it ran.** The first pass ran all four servers at 1 and 10 bots, but its Paper 10-bot run hit the
|
||||
connection throttle (1 of 10 bots joined), and it was stopped after that. The 1-bot and 10-bot lines for
|
||||
Horizon, vanilla and Fabric come from that pass, along with Paper's 1-bot line. A second pass with the
|
||||
throttle off ran all four at 25 and 50 bots, then Paper at 10. All runs were on the same machine on the
|
||||
same morning (02:57 to 06:02 UTC). `runs.jsonl` holds the 16 run lines used here.
|
||||
`report-25-50.json` and `report-paper-10.json` are the second pass's reports. The first pass, stopped
|
||||
early, wrote no report. Its lines come from its output.
|
||||
|
||||
## Reproduce
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
|
||||
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
|
||||
cargo build --release -p horizon-bots # on the machine that runs the bots
|
||||
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
|
||||
HORIZON_BENCH_BIN=~/.local/share/horizon/current \
|
||||
python3 tools/server_bench.py --servers horizon,vanilla,paper,fabric --bots 1,10,25,50 --behaviour mixed \
|
||||
--explore-speed 0.8 --warmup 300 --nice 19 \
|
||||
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
|
||||
--out four-way.json
|
||||
|
||||
## Next
|
||||
|
||||
- One pass per configuration ran, on a 4-thread laptop CPU. A rerun on a bigger machine (the desktop's
|
||||
24 threads) would show which server uses more cores best, and give vanilla at 50 a second chance.
|
||||
- Horizon's average tick: where its 1-player tick goes (13.7 ms against Paper's 10.3). That work is
|
||||
perf-server-11's.
|
||||
- 100 bots.
|
||||
@@ -1,870 +0,0 @@
|
||||
2026-10-19T02:57:31Z
|
||||
ludothinkpad ok: 29.0 GB free, 4 threads 1% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.7 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T02:58:36Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 20% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.0 GB free, 14 threads 92% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T02:59:40Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 20% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.8 GB free, 14 threads 96% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:00:44Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 15% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.8 GB free, 14 threads 97% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:01:48Z
|
||||
ludothinkpad ok: 27.8 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:02:52Z
|
||||
ludothinkpad ok: 28.4 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.7 GB free, 14 threads 93% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:03:56Z
|
||||
ludothinkpad ok: 28.4 GB free, 4 threads 14% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.6 GB free, 14 threads 96% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:05:00Z
|
||||
ludothinkpad ok: 28.4 GB free, 4 threads 11% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.4 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:06:04Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 22% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:07:09Z
|
||||
ludothinkpad ok: 28.4 GB free, 4 threads 11% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.1 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:08:13Z
|
||||
ludothinkpad busy: 26.9 GB free, 4 threads 92% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.4 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:09:17Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.5 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:10:21Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 49% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.8 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:11:25Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:12:29Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:13:33Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.0 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:14:37Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.7 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:15:41Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:16:46Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.0 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:17:50Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.6 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:18:54Z
|
||||
ludothinkpad ok: 26.7 GB free, 4 threads 71% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:19:58Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 16% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.6 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:21:02Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 15% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.7 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:22:06Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 12% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:23:10Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 12% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.6 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:24:14Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 19% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:25:19Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 9% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.6 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:26:23Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 13% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.9 GB free, 14 threads 71% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:27:27Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 14% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.0 GB free, 14 threads 94% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:28:31Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 13% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.1 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:29:35Z
|
||||
ludothinkpad ok: 27.5 GB free, 4 threads 36% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.5 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:30:39Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 22% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.2 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:31:43Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 20% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.1 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:32:47Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 21% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.4 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:33:52Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.9 GB free, 14 threads 84% busy, 2 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:34:56Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 14% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.1 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:36:00Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 16% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.4 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:37:04Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 15% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.6 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:38:08Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 14% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.8 GB free, 14 threads 94% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:39:12Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:40:16Z
|
||||
ludothinkpad ok: 28.4 GB free, 4 threads 73% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.5 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:41:20Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 41% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.8 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:42:24Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 24% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.9 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:43:29Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 26% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.0 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:44:33Z
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 22% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.6 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:45:37Z
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.8 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:46:41Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 37% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:47:45Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 21% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:48:49Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 32% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.2 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:49:53Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 25% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.7 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:50:57Z
|
||||
ludothinkpad busy: 26.6 GB free, 4 threads 94% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.1 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:52:01Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 28% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.1 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:53:06Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 50% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.4 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:54:10Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 41% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:55:14Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.4 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:56:18Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 26% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.0 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:57:22Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 33% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.5 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:58:26Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 45% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T03:59:30Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 24% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.6 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:00:34Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 31% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.0 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:01:38Z
|
||||
ludothinkpad busy: 27.0 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.3 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:02:42Z
|
||||
ludothinkpad ok: 26.2 GB free, 4 threads 19% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.5 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:03:47Z
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 14% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.9 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:04:51Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 11% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 30.0 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:05:55Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 10% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.5 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:06:59Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 13% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.9 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:08:03Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 12% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.8 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:09:07Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 10% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.9 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:10:11Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 8% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.6 GB free, 14 threads 92% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:11:15Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 9% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:12:20Z
|
||||
ludothinkpad busy: 27.5 GB free, 4 threads 88% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.5 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:13:24Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 44% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.6 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:14:28Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 28% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:15:32Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 26% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.7 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:16:36Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 28% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.3 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:17:40Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 32% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.3 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:18:44Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 76% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.4 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:19:48Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 26% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.4 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:20:52Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 26% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.1 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:21:57Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 28% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.0 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:23:01Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 10% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.7 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:24:05Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 41% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 71% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:25:15Z
|
||||
ludothinkpad ok: 28.8 GB free, 4 threads 4% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.7 GB free, 14 threads 98% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:26:19Z
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 40% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 32.1 GB free, 14 threads 65% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:27:23Z
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 41% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.4 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:28:27Z
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 44% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.9 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:29:32Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 33% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.3 GB free, 14 threads 71% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:30:36Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 31% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.0 GB free, 14 threads 63% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:31:40Z
|
||||
ludothinkpad ok: 28.0 GB free, 4 threads 40% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.4 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:32:44Z
|
||||
ludothinkpad ok: 28.0 GB free, 4 threads 35% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.3 GB free, 14 threads 51% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:33:48Z
|
||||
ludothinkpad ok: 28.0 GB free, 4 threads 31% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.5 GB free, 14 threads 67% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:34:52Z
|
||||
ludothinkpad ok: 28.0 GB free, 4 threads 35% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.6 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:35:56Z
|
||||
ludothinkpad ok: 27.9 GB free, 4 threads 34% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.9 GB free, 14 threads 66% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:37:00Z
|
||||
ludothinkpad busy: 25.8 GB free, 4 threads 98% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.9 GB free, 14 threads 75% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:38:04Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 64% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.6 GB free, 14 threads 74% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:39:09Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 64% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:40:13Z
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 51% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.3 GB free, 14 threads 70% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:41:17Z
|
||||
ludothinkpad ok: 25.4 GB free, 4 threads 62% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.3 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:42:21Z
|
||||
ludothinkpad ok: 25.4 GB free, 4 threads 47% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.2 GB free, 14 threads 68% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:43:25Z
|
||||
ludothinkpad ok: 25.4 GB free, 4 threads 67% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.9 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:44:29Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 64% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.4 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:45:33Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 66% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.0 GB free, 14 threads 69% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:46:37Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 48% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.4 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:47:42Z
|
||||
ludothinkpad ok: 27.5 GB free, 4 threads 69% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.0 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:48:46Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 83% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.6 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:49:50Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 55% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.3 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:50:54Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 51% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.9 GB free, 14 threads 74% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:51:58Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 57% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.2 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:53:02Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 59% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:54:06Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 38% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.4 GB free, 14 threads 90% busy, 2 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:55:10Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 43% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.4 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:56:15Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 42% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.6 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:57:19Z
|
||||
ludothinkpad ok: 25.0 GB free, 4 threads 45% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.0 GB free, 14 threads 67% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:58:23Z
|
||||
ludothinkpad ok: 25.0 GB free, 4 threads 48% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.4 GB free, 14 threads 74% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T04:59:27Z
|
||||
ludothinkpad busy: 26.6 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.7 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:00:31Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 53% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 32.2 GB free, 14 threads 64% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:01:35Z
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 54% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.1 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:02:39Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 59% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.4 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:03:43Z
|
||||
ludothinkpad ok: 25.6 GB free, 4 threads 55% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.3 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:04:48Z
|
||||
ludothinkpad busy: 25.0 GB free, 4 threads 96% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.6 GB free, 14 threads 79% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:05:52Z
|
||||
ludothinkpad ok: 25.4 GB free, 4 threads 51% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.8 GB free, 14 threads 84% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:06:56Z
|
||||
ludothinkpad ok: 25.4 GB free, 4 threads 46% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.9 GB free, 14 threads 92% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:08:00Z
|
||||
ludothinkpad ok: 25.4 GB free, 4 threads 53% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.0 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:09:04Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 64% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:10:08Z
|
||||
ludothinkpad ok: 28.6 GB free, 4 threads 28% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.8 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:11:12Z
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 48% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.5 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:12:16Z
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 52% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 74% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:13:21Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 41% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.9 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:14:25Z
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:15:29Z
|
||||
ludothinkpad ok: 28.0 GB free, 4 threads 50% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.4 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:16:33Z
|
||||
ludothinkpad ok: 27.9 GB free, 4 threads 47% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.1 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:17:37Z
|
||||
ludothinkpad ok: 27.9 GB free, 4 threads 61% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.5 GB free, 14 threads 75% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:18:41Z
|
||||
ludothinkpad ok: 27.9 GB free, 4 threads 68% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:19:45Z
|
||||
ludothinkpad ok: 27.8 GB free, 4 threads 45% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.5 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:20:49Z
|
||||
ludothinkpad ok: 27.5 GB free, 4 threads 60% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.6 GB free, 14 threads 95% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:21:53Z
|
||||
ludothinkpad busy: 26.7 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.8 GB free, 14 threads 72% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:22:58Z
|
||||
ludothinkpad busy: 25.7 GB free, 4 threads 97% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.6 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:24:02Z
|
||||
ludothinkpad busy: 25.4 GB free, 4 threads 90% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.2 GB free, 14 threads 85% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:25:06Z
|
||||
ludothinkpad ok: 25.3 GB free, 4 threads 73% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.8 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:26:10Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 69% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.8 GB free, 14 threads 74% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:27:14Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 82% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.6 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:28:18Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 66% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 33.7 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:29:22Z
|
||||
ludothinkpad busy: 26.4 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 33.7 GB free, 14 threads 81% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:30:27Z
|
||||
ludothinkpad ok: 25.0 GB free, 4 threads 73% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:31:31Z
|
||||
ludothinkpad ok: 24.9 GB free, 4 threads 66% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.7 GB free, 14 threads 77% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:32:35Z
|
||||
ludothinkpad busy: 25.0 GB free, 4 threads 94% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:33:39Z
|
||||
ludothinkpad ok: 24.9 GB free, 4 threads 70% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.7 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:34:43Z
|
||||
ludothinkpad busy: 24.9 GB free, 4 threads 88% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 32.7 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:35:47Z
|
||||
ludothinkpad ok: 24.9 GB free, 4 threads 72% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 33.3 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:36:51Z
|
||||
ludothinkpad ok: 24.8 GB free, 4 threads 76% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 32.6 GB free, 14 threads 83% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:37:55Z
|
||||
ludothinkpad ok: 24.7 GB free, 4 threads 71% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.5 GB free, 14 threads 65% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:39:00Z
|
||||
ludothinkpad busy: 24.7 GB free, 4 threads 92% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.7 GB free, 14 threads 69% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:40:04Z
|
||||
ludothinkpad ok: 28.6 GB free, 4 threads 55% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.0 GB free, 14 threads 73% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:41:08Z
|
||||
ludothinkpad busy: 25.6 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.3 GB free, 14 threads 88% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:42:12Z
|
||||
ludothinkpad busy: 25.5 GB free, 4 threads 97% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.6 GB free, 14 threads 80% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:43:16Z
|
||||
ludothinkpad busy: 25.3 GB free, 4 threads 98% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.5 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:44:20Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 85% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.6 GB free, 14 threads 64% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:45:25Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 74% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.6 GB free, 14 threads 78% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:46:29Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 68% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.5 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:47:33Z
|
||||
ludothinkpad ok: 24.5 GB free, 4 threads 84% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.5 GB free, 14 threads 90% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:48:37Z
|
||||
ludothinkpad busy: 24.4 GB free, 4 threads 86% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.6 GB free, 14 threads 61% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:49:41Z
|
||||
ludothinkpad ok: 24.2 GB free, 4 threads 80% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.9 GB free, 14 threads 85% busy, 3 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:50:45Z
|
||||
ludothinkpad ok: 24.2 GB free, 4 threads 72% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 25.7 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:51:49Z
|
||||
ludothinkpad ok: 24.1 GB free, 4 threads 17% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.1 GB free, 14 threads 87% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:52:54Z
|
||||
ludothinkpad ok: 27.5 GB free, 4 threads 83% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.9 GB free, 14 threads 76% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:53:58Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 82% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:55:02Z
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 35% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.2 GB free, 14 threads 95% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:56:06Z
|
||||
ludothinkpad ok: 25.0 GB free, 4 threads 52% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.6 GB free, 14 threads 56% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:57:10Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 30% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.2 GB free, 14 threads 91% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:58:14Z
|
||||
ludothinkpad ok: 25.1 GB free, 4 threads 36% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.8 GB free, 14 threads 94% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T05:59:18Z
|
||||
ludothinkpad ok: 25.0 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 30.3 GB free, 14 threads 51% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T06:00:22Z
|
||||
ludothinkpad ok: 25.0 GB free, 4 threads 24% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 86% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T06:01:26Z
|
||||
ludothinkpad ok: 24.4 GB free, 4 threads 38% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.1 GB free, 14 threads 89% busy, 1 sessions
|
||||
macbook offline
|
||||
2026-10-19T06:02:31Z
|
||||
ludothinkpad ok: 25.2 GB free, 4 threads 27% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.1 GB free, 14 threads 55% busy, 1 sessions
|
||||
macbook offline
|
||||
@@ -1,437 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-19T05:52:44+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "38505a07b5334f3ea2c467efb4a00e5557f910f8",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"versions": {
|
||||
"paper_build": 205,
|
||||
"fabric_loader": "0.19.5",
|
||||
"lithium": "mc26.3-0.26.2-fabric"
|
||||
},
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla,paper,fabric",
|
||||
"bots": "25,50",
|
||||
"behaviour": "mixed",
|
||||
"join_every": 1.0,
|
||||
"warmup": 300,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0.8,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/sh10/out/mixed-equal-25-50.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/sh10-bench/target/release/horizon-bots",
|
||||
"connect": "the ThinkPad's tailnet address",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 8.51,
|
||||
"mspt": {
|
||||
"average": 30.92,
|
||||
"p50": 27.88,
|
||||
"p95": 48.25,
|
||||
"p99": 72.0
|
||||
},
|
||||
"mspt_worst_p99": 121.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1012.01,
|
||||
"max": 1121.3
|
||||
},
|
||||
"cpu_percent": 98.57,
|
||||
"chunks_per_second": 155.81,
|
||||
"unique_per_second": 5.78,
|
||||
"forgotten_per_second": 155.62,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 106268,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6506.33,
|
||||
"explore": 1261.6,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 702.13,
|
||||
"explore": 1184.4,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3008,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 15.47,
|
||||
"mspt": {
|
||||
"average": 37.35,
|
||||
"p50": 35.64,
|
||||
"p95": 48.2,
|
||||
"p99": 82.66
|
||||
},
|
||||
"mspt_worst_p99": 579.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3935.5,
|
||||
"max": 4157.4
|
||||
},
|
||||
"cpu_percent": 191.44,
|
||||
"chunks_per_second": 158.52,
|
||||
"unique_per_second": 5.71,
|
||||
"forgotten_per_second": 158.39,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 103148,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6306.87,
|
||||
"explore": 1236.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 704.2,
|
||||
"explore": 1160.2,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3762,
|
||||
"moved_too_quickly": 11,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "paper",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 18.95,
|
||||
"mspt": {
|
||||
"average": 28.3,
|
||||
"p50": 23.7,
|
||||
"p95": 61.68,
|
||||
"p99": 90.4
|
||||
},
|
||||
"mspt_worst_p99": 124.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4160.34,
|
||||
"max": 4246.4
|
||||
},
|
||||
"cpu_percent": 124.24,
|
||||
"chunks_per_second": 156.35,
|
||||
"unique_per_second": 5.72,
|
||||
"forgotten_per_second": 156.25,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 102715,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6280.4,
|
||||
"explore": 1228.8,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 703.07,
|
||||
"explore": 1155.6,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 4337,
|
||||
"moved_too_quickly": 9,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "fabric",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 17.45,
|
||||
"mspt": {
|
||||
"average": 31.44,
|
||||
"p50": 28.33,
|
||||
"p95": 52.45,
|
||||
"p99": 93.35
|
||||
},
|
||||
"mspt_worst_p99": 409.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3884.61,
|
||||
"max": 4186.4
|
||||
},
|
||||
"cpu_percent": 174.04,
|
||||
"chunks_per_second": 158.89,
|
||||
"unique_per_second": 5.75,
|
||||
"forgotten_per_second": 158.54,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 103913,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6356.8,
|
||||
"explore": 1239.2,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 704.27,
|
||||
"explore": 1163.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 4194,
|
||||
"moved_too_quickly": 11,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 8.31,
|
||||
"mspt": {
|
||||
"average": 47.83,
|
||||
"p50": 46.02,
|
||||
"p95": 62.42,
|
||||
"p99": 73.21
|
||||
},
|
||||
"mspt_worst_p99": 133.0,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1112.46,
|
||||
"max": 1296.4
|
||||
},
|
||||
"cpu_percent": 143.22,
|
||||
"chunks_per_second": 302.68,
|
||||
"unique_per_second": 11.02,
|
||||
"forgotten_per_second": 303.15,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 211296,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6468.47,
|
||||
"explore": 1251.2,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 701.67,
|
||||
"explore": 1180.5,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 8562,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 15.45,
|
||||
"error": "[Errno 32] Broken pipe"
|
||||
},
|
||||
{
|
||||
"server": "paper",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 19.35,
|
||||
"mspt": {
|
||||
"average": 46.86,
|
||||
"p50": 45.11,
|
||||
"p95": 61.16,
|
||||
"p99": 109.72
|
||||
},
|
||||
"mspt_worst_p99": 213.4,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4353.02,
|
||||
"max": 4516.1
|
||||
},
|
||||
"cpu_percent": 210.21,
|
||||
"chunks_per_second": 310.51,
|
||||
"unique_per_second": 11.16,
|
||||
"forgotten_per_second": 310.64,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 209303,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6411.0,
|
||||
"explore": 1224.3,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 703.47,
|
||||
"explore": 1155.8,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 7344,
|
||||
"moved_too_quickly": 35,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "fabric",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 17.45,
|
||||
"mspt": {
|
||||
"average": 58.3,
|
||||
"p50": 54.73,
|
||||
"p95": 83.06,
|
||||
"p99": 174.37
|
||||
},
|
||||
"mspt_worst_p99": 887.6,
|
||||
"tps": 17.15,
|
||||
"rss_mb": {
|
||||
"mean": 4521.47,
|
||||
"max": 4744.2
|
||||
},
|
||||
"cpu_percent": 236.04,
|
||||
"chunks_per_second": 311.52,
|
||||
"unique_per_second": 11.28,
|
||||
"forgotten_per_second": 311.74,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 206965,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6331.7,
|
||||
"explore": 1228.4,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 703.3,
|
||||
"explore": 1160.3,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 6350,
|
||||
"moved_too_quickly": 46,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
}
|
||||
],
|
||||
"work": [
|
||||
{
|
||||
"behaviour": "mixed",
|
||||
"bots": 25,
|
||||
"servers": {
|
||||
"horizon": {
|
||||
"chunks_per_second": {
|
||||
"value": 155.81,
|
||||
"share": 0.981
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 5.78,
|
||||
"share": 1.0
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"vanilla": {
|
||||
"chunks_per_second": {
|
||||
"value": 158.52,
|
||||
"share": 0.998
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 5.71,
|
||||
"share": 0.988
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"paper": {
|
||||
"chunks_per_second": {
|
||||
"value": 156.35,
|
||||
"share": 0.984
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 5.72,
|
||||
"share": 0.99
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"fabric": {
|
||||
"chunks_per_second": {
|
||||
"value": 158.89,
|
||||
"share": 1.0
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 5.75,
|
||||
"share": 0.995
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
}
|
||||
},
|
||||
"equal": true
|
||||
},
|
||||
{
|
||||
"behaviour": "mixed",
|
||||
"bots": 50,
|
||||
"servers": {
|
||||
"horizon": {
|
||||
"chunks_per_second": {
|
||||
"value": 302.68,
|
||||
"share": 0.972
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 11.02,
|
||||
"share": 0.977
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"paper": {
|
||||
"chunks_per_second": {
|
||||
"value": 310.51,
|
||||
"share": 0.997
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 11.16,
|
||||
"share": 0.989
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
},
|
||||
"fabric": {
|
||||
"chunks_per_second": {
|
||||
"value": 311.52,
|
||||
"share": 1.0
|
||||
},
|
||||
"unique_per_second": {
|
||||
"value": 11.28,
|
||||
"share": 1.0
|
||||
},
|
||||
"waited_ticks_per_second": 0.0
|
||||
}
|
||||
},
|
||||
"equal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-19T06:03:34+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4
|
||||
},
|
||||
"commit": "38505a07b5334f3ea2c467efb4a00e5557f910f8",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"versions": {
|
||||
"paper_build": 205
|
||||
},
|
||||
"settings": {
|
||||
"servers": "paper",
|
||||
"bots": "10",
|
||||
"behaviour": "mixed",
|
||||
"join_every": 1.0,
|
||||
"warmup": 300,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0.8,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "/home/ludovic/sh10/out/mixed-equal-paper-10.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/sh10-bench/target/release/horizon-bots",
|
||||
"connect": "the ThinkPad's tailnet address",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "paper",
|
||||
"bots": 10,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 20.48,
|
||||
"mspt": {
|
||||
"average": 20.64,
|
||||
"p50": 18.1,
|
||||
"p95": 35.94,
|
||||
"p99": 56.29
|
||||
},
|
||||
"mspt_worst_p99": 166.9,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 4069.94,
|
||||
"max": 4080.2
|
||||
},
|
||||
"cpu_percent": 73.56,
|
||||
"chunks_per_second": 60.76,
|
||||
"unique_per_second": 1.85,
|
||||
"forgotten_per_second": 60.96,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 40348,
|
||||
"chunks_per_bot": {
|
||||
"walk": 6176.67,
|
||||
"explore": 1171.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 701.0,
|
||||
"explore": 1091.5,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 3974,
|
||||
"moved_too_quickly": 7,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 59
|
||||
}
|
||||
],
|
||||
"work": []
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{"server": "horizon", "bots": 1, "behaviour": "mixed", "start_seconds": 8.31, "mspt": {"average": 13.7, "p50": 14.18, "p95": 19.06, "p99": 27.38}, "mspt_worst_p99": 43.4, "tps": 20.0, "rss_mb": {"mean": 715.63, "max": 719.6}, "cpu_percent": 46.22, "chunks_per_second": 9.94, "unique_per_second": 0.01, "forgotten_per_second": 9.89, "waited_ticks_per_second": 0.0, "chunks_total": 6280, "chunks_per_bot": {"walk": 6280.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "vanilla", "bots": 1, "behaviour": "mixed", "start_seconds": 14.85, "mspt": {"average": 17.31, "p50": 18.3, "p95": 24.68, "p99": 36.43}, "mspt_worst_p99": 71.0, "tps": 20.0, "rss_mb": {"mean": 3442.51, "max": 3444.5}, "cpu_percent": 55.03, "chunks_per_second": 9.99, "unique_per_second": 0.0, "forgotten_per_second": 9.95, "waited_ticks_per_second": 0.0, "chunks_total": 6210, "chunks_per_bot": {"walk": 6210.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "paper", "bots": 1, "behaviour": "mixed", "start_seconds": 22.27, "mspt": {"average": 10.31, "p50": 10.4, "p95": 13.9, "p99": 22.5}, "mspt_worst_p99": 81.2, "tps": 20.0, "rss_mb": {"mean": 4239.22, "max": 4245.1}, "cpu_percent": 31.68, "chunks_per_second": 9.81, "unique_per_second": 0.02, "forgotten_per_second": 9.77, "waited_ticks_per_second": 0.0, "chunks_total": 6049, "chunks_per_bot": {"walk": 6049.0}, "unique_chunks_per_bot": {"walk": 698.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "fabric", "bots": 1, "behaviour": "mixed", "start_seconds": 17.55, "mspt": {"average": 12.46, "p50": 12.62, "p95": 17.69, "p99": 32.74}, "mspt_worst_p99": 70.1, "tps": 20.0, "rss_mb": {"mean": 3428.55, "max": 3430.2}, "cpu_percent": 45.86, "chunks_per_second": 9.98, "unique_per_second": 0.0, "forgotten_per_second": 9.93, "waited_ticks_per_second": 0.0, "chunks_total": 6188, "chunks_per_bot": {"walk": 6188.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "horizon", "bots": 10, "behaviour": "mixed", "start_seconds": 8.21, "mspt": {"average": 22.93, "p50": 20.03, "p95": 39.83, "p99": 53.33}, "mspt_worst_p99": 89.5, "tps": 20.0, "rss_mb": {"mean": 922.28, "max": 973.7}, "cpu_percent": 66.6, "chunks_per_second": 61.21, "unique_per_second": 1.78, "forgotten_per_second": 61.34, "waited_ticks_per_second": 0.0, "chunks_total": 42095, "chunks_per_bot": {"walk": 6460.0, "explore": 1194.5, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.17, "explore": 1112.5, "idle": 473.0}, "resent": 0, "pulled_back": 3702, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "vanilla", "bots": 10, "behaviour": "mixed", "start_seconds": 14.65, "mspt": {"average": 22.94, "p50": 20.61, "p95": 36.92, "p99": 56.48}, "mspt_worst_p99": 196.6, "tps": 20.0, "rss_mb": {"mean": 3706.57, "max": 3761.6}, "cpu_percent": 100.83, "chunks_per_second": 61.63, "unique_per_second": 1.84, "forgotten_per_second": 61.68, "waited_ticks_per_second": 0.0, "chunks_total": 40938, "chunks_per_bot": {"walk": 6272.17, "explore": 1179.5, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.0, "explore": 1097.0, "idle": 473.0}, "resent": 0, "pulled_back": 3370, "moved_too_quickly": 7, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
|
||||
{"server": "paper", "bots": 10, "behaviour": "mixed", "start_seconds": 20.48, "mspt": {"average": 20.64, "p50": 18.1, "p95": 35.94, "p99": 56.29}, "mspt_worst_p99": 166.9, "tps": 20.0, "rss_mb": {"mean": 4069.94, "max": 4080.2}, "cpu_percent": 73.56, "chunks_per_second": 60.76, "unique_per_second": 1.85, "forgotten_per_second": 60.96, "waited_ticks_per_second": 0.0, "chunks_total": 40348, "chunks_per_bot": {"walk": 6176.67, "explore": 1171.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.0, "explore": 1091.5, "idle": 473.0}, "resent": 0, "pulled_back": 3974, "moved_too_quickly": 7, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
|
||||
{"server": "fabric", "bots": 10, "behaviour": "mixed", "start_seconds": 16.64, "mspt": {"average": 20.18, "p50": 17.63, "p95": 37.04, "p99": 56.08}, "mspt_worst_p99": 217.9, "tps": 20.0, "rss_mb": {"mean": 3732.43, "max": 3776.5}, "cpu_percent": 94.48, "chunks_per_second": 61.65, "unique_per_second": 1.84, "forgotten_per_second": 61.76, "waited_ticks_per_second": 0.0, "chunks_total": 41055, "chunks_per_bot": {"walk": 6291.17, "explore": 1181.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.83, "explore": 1098.0, "idle": 473.0}, "resent": 0, "pulled_back": 4002, "moved_too_quickly": 3, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
|
||||
{"server": "horizon", "bots": 25, "behaviour": "mixed", "start_seconds": 8.51, "mspt": {"average": 30.92, "p50": 27.88, "p95": 48.25, "p99": 72.0}, "mspt_worst_p99": 121.1, "tps": 20.0, "rss_mb": {"mean": 1012.01, "max": 1121.3}, "cpu_percent": 98.57, "chunks_per_second": 155.81, "unique_per_second": 5.78, "forgotten_per_second": 155.62, "waited_ticks_per_second": 0.0, "chunks_total": 106268, "chunks_per_bot": {"walk": 6506.33, "explore": 1261.6, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.13, "explore": 1184.4, "idle": 473.0}, "resent": 0, "pulled_back": 3008, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "vanilla", "bots": 25, "behaviour": "mixed", "start_seconds": 15.47, "mspt": {"average": 37.35, "p50": 35.64, "p95": 48.2, "p99": 82.66}, "mspt_worst_p99": 579.8, "tps": 20.0, "rss_mb": {"mean": 3935.5, "max": 4157.4}, "cpu_percent": 191.44, "chunks_per_second": 158.52, "unique_per_second": 5.71, "forgotten_per_second": 158.39, "waited_ticks_per_second": 0.0, "chunks_total": 103148, "chunks_per_bot": {"walk": 6306.87, "explore": 1236.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 704.2, "explore": 1160.2, "idle": 473.0}, "resent": 0, "pulled_back": 3762, "moved_too_quickly": 11, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
|
||||
{"server": "paper", "bots": 25, "behaviour": "mixed", "start_seconds": 18.95, "mspt": {"average": 28.3, "p50": 23.7, "p95": 61.68, "p99": 90.4}, "mspt_worst_p99": 124.3, "tps": 20.0, "rss_mb": {"mean": 4160.34, "max": 4246.4}, "cpu_percent": 124.24, "chunks_per_second": 156.35, "unique_per_second": 5.72, "forgotten_per_second": 156.25, "waited_ticks_per_second": 0.0, "chunks_total": 102715, "chunks_per_bot": {"walk": 6280.4, "explore": 1228.8, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.07, "explore": 1155.6, "idle": 473.0}, "resent": 0, "pulled_back": 4337, "moved_too_quickly": 9, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
|
||||
{"server": "fabric", "bots": 25, "behaviour": "mixed", "start_seconds": 17.45, "mspt": {"average": 31.44, "p50": 28.33, "p95": 52.45, "p99": 93.35}, "mspt_worst_p99": 409.3, "tps": 20.0, "rss_mb": {"mean": 3884.61, "max": 4186.4}, "cpu_percent": 174.04, "chunks_per_second": 158.89, "unique_per_second": 5.75, "forgotten_per_second": 158.54, "waited_ticks_per_second": 0.0, "chunks_total": 103913, "chunks_per_bot": {"walk": 6356.8, "explore": 1239.2, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 704.27, "explore": 1163.0, "idle": 473.0}, "resent": 0, "pulled_back": 4194, "moved_too_quickly": 11, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
|
||||
{"server": "horizon", "bots": 50, "behaviour": "mixed", "start_seconds": 8.31, "mspt": {"average": 47.83, "p50": 46.02, "p95": 62.42, "p99": 73.21}, "mspt_worst_p99": 133.0, "tps": 20.0, "rss_mb": {"mean": 1112.46, "max": 1296.4}, "cpu_percent": 143.22, "chunks_per_second": 302.68, "unique_per_second": 11.02, "forgotten_per_second": 303.15, "waited_ticks_per_second": 0.0, "chunks_total": 211296, "chunks_per_bot": {"walk": 6468.47, "explore": 1251.2, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.67, "explore": 1180.5, "idle": 473.0}, "resent": 0, "pulled_back": 8562, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "vanilla", "bots": 50, "behaviour": "mixed", "start_seconds": 15.45, "error": "[Errno 32] Broken pipe"}
|
||||
{"server": "paper", "bots": 50, "behaviour": "mixed", "start_seconds": 19.35, "mspt": {"average": 46.86, "p50": 45.11, "p95": 61.16, "p99": 109.72}, "mspt_worst_p99": 213.4, "tps": 20.0, "rss_mb": {"mean": 4353.02, "max": 4516.1}, "cpu_percent": 210.21, "chunks_per_second": 310.51, "unique_per_second": 11.16, "forgotten_per_second": 310.64, "waited_ticks_per_second": 0.0, "chunks_total": 209303, "chunks_per_bot": {"walk": 6411.0, "explore": 1224.3, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.47, "explore": 1155.8, "idle": 473.0}, "resent": 0, "pulled_back": 7344, "moved_too_quickly": 35, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
|
||||
{"server": "fabric", "bots": 50, "behaviour": "mixed", "start_seconds": 17.45, "mspt": {"average": 58.3, "p50": 54.73, "p95": 83.06, "p99": 174.37}, "mspt_worst_p99": 887.6, "tps": 17.15, "rss_mb": {"mean": 4521.47, "max": 4744.2}, "cpu_percent": 236.04, "chunks_per_second": 311.52, "unique_per_second": 11.28, "forgotten_per_second": 311.74, "waited_ticks_per_second": 0.0, "chunks_total": 206965, "chunks_per_bot": {"walk": 6331.7, "explore": 1228.4, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.3, "explore": 1160.3, "idle": 473.0}, "resent": 0, "pulled_back": 6350, "moved_too_quickly": 46, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
|
||||
@@ -1,125 +0,0 @@
|
||||
# Horizon's server against vanilla 26.3: mixed bots (2026-10-18)
|
||||
|
||||
The first benchmark from docs/server.md section 10 (item 4): the same bots on Horizon's dedicated server, as
|
||||
`install.sh` installs it today, and on vanilla 26.3's, one server at a time on the same quiet machine.
|
||||
There were 1, 10, 25 and 50 bots, all with the `mixed` behaviour.
|
||||
|
||||
## Summary
|
||||
|
||||
- **Both servers held 20 TPS in every run.**
|
||||
- **Memory:** Horizon's RSS was 3.7 to 5.2 times smaller: 0.65 GB against 3.4 GB with one player, and
|
||||
1.0 GB against 4.0 GB with 50. Vanilla runs with the usual fixed heap (`-Xms4g -Xmx4g`), so its RSS
|
||||
is mostly that heap.
|
||||
- **Tick time (MSPT):**
|
||||
- with 1, 10 and 25 players, Horizon's average tick was shorter (2.4 against 17.9 ms, 12.4 against 34.5,
|
||||
24.0 against 31.6);
|
||||
- with 50, it was longer (35.2 against 29.7 ms);
|
||||
- Horizon's P99 was lower at every count, and so were its worst 5-second windows (241 ms at most,
|
||||
against 424 to 2320 ms for vanilla).
|
||||
- **The work was not equal, and the difference counts against Horizon.** In `mixed`, 20 % of the bots
|
||||
fly straight out at 22 blocks a second, as fast as each server sends them chunks. Horizon kept up:
|
||||
- with 10 or more players it sent 2.9 to 5.9 times as many chunks a second, and generated 1.9 to 2.9
|
||||
times as much new terrain a second;
|
||||
- its walking bots each received 3300 to 4000 chunks, against 640 to 1100 on vanilla once 10 or more
|
||||
were online.
|
||||
|
||||
So at 50 players Horizon's tick is 18 % longer while it generates twice the new terrain. The
|
||||
equal-rate explore pass of `../server-equal-work-2026-10-17/` is the fair comparison for exploring.
|
||||
- **Start (fresh world, to `Done`):** 10 to 20 s for Horizon, 14.5 to 28.6 s for vanilla.
|
||||
|
||||
## Results
|
||||
|
||||
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and
|
||||
P99 are over the whole run. "Worst P99" is the highest P99 of a single 5-second query. RSS and CPU cover
|
||||
the server's whole process tree (100 % is one thread; the machine has 4). "New chunks/s" counts chunks
|
||||
no bot had before. "Pulled back" counts position packets that moved a bot back.
|
||||
|
||||
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | New chunks/s | Pulled back |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1 | Horizon | **2.4** | 5.2 | 18.2 | 45.6 | 20 | **0.65 / 0.66 GB** | 25 % | 10.9 s | 9.9 | 0.3 | 0 |
|
||||
| 1 | vanilla | 17.9 | 27.6 | 47.3 | 423.8 | 20 | 3.38 / 3.39 GB | 64 % | 14.8 s | 9.9 | 0.3 | 0 |
|
||||
| 10 | Horizon | **12.4** | 36.0 | 65.9 | 117.1 | 20 | **0.94 / 1.01 GB** | 210 % | 10.2 s | 98.2 | 36.5 | 6267 |
|
||||
| 10 | vanilla | 34.5 | 63.5 | 138.6 | 884.7 | 20 | 3.59 / 3.66 GB | 217 % | 14.5 s | 24.3 | 19.1 | 5500 |
|
||||
| 25 | Horizon | **24.0** | 49.7 | 80.6 | 168.8 | 20 | **1.01 / 1.18 GB** | 192 % | 14.7 s | 178.2 | 31.9 | 5532 |
|
||||
| 25 | vanilla | 31.6 | 70.0 | 222.7 | 2319.6 | 20 | 3.74 / 4.06 GB | 218 % | 28.6 s | 30.4 | 10.8 | 4524 |
|
||||
| 50 | Horizon | 35.2 | 69.0 | 96.6 | 240.8 | 20 | **1.01 / 1.25 GB** | 218 % | 19.8 s | 273.2 | 34.6 | 8369 |
|
||||
| 50 | vanilla | **29.7** | 62.3 | 137.4 | 489.3 | 20 | 4.00 / 4.60 GB | 294 % | 20.6 s | 94.4 | 17.8 | 12034 |
|
||||
|
||||
Chunks each bot received, by behaviour (mean):
|
||||
|
||||
| Bots | Server | walk | explore | idle |
|
||||
|---:|---|---:|---:|---:|
|
||||
| 10 | Horizon | 3992 | 7360 | 473 |
|
||||
| 10 | vanilla | 680 | 2824 | 358 |
|
||||
| 25 | Horizon | 3928 | 2475 | 473 |
|
||||
| 25 | vanilla | 640 | 738 | 180 |
|
||||
| 50 | Horizon | 3300 | 1192 | 460 |
|
||||
| 50 | vanilla | 1105 | 629 | 220 |
|
||||
|
||||
No bot failed to join or was dropped, and no chunk was sent twice (`resent` 0), on either server. Vanilla
|
||||
also logged "moved too quickly" 12, 324 and 2193 times at 10, 25 and 50 bots.
|
||||
|
||||
## Method
|
||||
|
||||
`tools/server_bench.py`, unchanged, runs each configuration on a fresh server:
|
||||
|
||||
- the same `server.properties` on both: seed 12345, view and simulation distance 10, offline, creative,
|
||||
difficulty easy, `sync-chunk-writes=true`, white-list off;
|
||||
- `gamerule respawn_radius 0`, and otherwise vanilla's default game rules on both. The locator bar is on
|
||||
for both: Horizon's server has run it since v2026.10.18.13. The 2026-10-17 runs turned it off on vanilla
|
||||
because Horizon had none then;
|
||||
- the server waits for `Done`, then the bots join one a second, warm up for 60 seconds, and are measured
|
||||
for 300 seconds, with `/tick query` every 5 seconds and RSS and CPU every second.
|
||||
|
||||
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice
|
||||
deterministic per bot. All are creative and fly 24 blocks above their spawn:
|
||||
|
||||
- `walk`: random points within 48 blocks at 5.6 blocks a second;
|
||||
- `explore`: straight out at up to 22 blocks a second, as fast as chunks come;
|
||||
- `idle`: hovers.
|
||||
|
||||
A bot doesn't move while the chunk it stands in hasn't arrived, and it acknowledges chunk batches as
|
||||
vanilla's client does.
|
||||
|
||||
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS (Linux 6.18). No other
|
||||
session ran on it (`load.txt`: the fleet monitor shows 0 sessions throughout). The busy periods are the
|
||||
servers themselves, which used 2 to 3 of its 4 threads from 10 bots up, so both servers were CPU-bound
|
||||
there. The servers ran under `nice -n 19`. The desktop was meant to run this (24 threads) but didn't
|
||||
wake. The bots ran on the Mac mini (M4 Pro) and reached the ThinkPad over the tailnet, so they took none
|
||||
of the servers' CPU. The Mac mini was shared with other work (70 to 90 % busy), which only affects the
|
||||
bots' timing.
|
||||
|
||||
**What ran.**
|
||||
|
||||
- Horizon: the published v2026.10.18.15 release, `horizon-client` (the engine) at 6c078b28, as
|
||||
`install.sh` installs it. Its game files were prepared from the owner's own Minecraft by the release's
|
||||
`horizon-prepare`. `horizon-server` (the supervisor, which takes no part in the tick) came from #898
|
||||
(4ea2243e), because the release's copy can't find game files prepared without sounds (#893).
|
||||
`horizon-bots` came from main.
|
||||
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
|
||||
|
||||
On Linux the release's engine needs `libasound.so.2` and `libudev.so.1`. The ThinkPad has them only in
|
||||
the Nix store, so the run set `LD_LIBRARY_PATH` to them (see #898).
|
||||
|
||||
## Reproduce
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
|
||||
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
|
||||
cargo build --release -p horizon-bots # on the machine that runs the bots
|
||||
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
|
||||
HORIZON_BENCH_BIN=<a folder with horizon-server and horizon-client> \
|
||||
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50 --behaviour mixed --nice 19 \
|
||||
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
|
||||
--out mixed.json
|
||||
|
||||
`mixed.json` is the report (`horizon-server-benchmark`: machine, versions, settings and every run), and
|
||||
`load.txt` is the ThinkPad's load before and during the run.
|
||||
|
||||
## Next
|
||||
|
||||
- An equal-work `mixed` pass (`--explore-speed` at a rate both servers keep up with), then 100 bots.
|
||||
- The same run on a bigger machine, so neither server is CPU-bound at 25 to 50 bots.
|
||||
- Paper and Fabric + Lithium (docs/server.md section 9).
|
||||
- At 50 players Horizon's tick is longer than vanilla's while it generates and sends far more. The
|
||||
profile of the 2026-10-17 run (streaming and block entity ticks growing with players) is
|
||||
perf-server's to take on.
|
||||
@@ -1,286 +0,0 @@
|
||||
ludothinkpad — OK
|
||||
RAM 29.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
|
||||
CPU 4 threads, 2% busy (agents 0.0, rest 0.1 cores) · 67°C · load 2.1
|
||||
· disk 295.5 GB free · battery 96%
|
||||
|
||||
Sessions, most important first:
|
||||
(finished session) 0.0 GB leftovers: node 0.0 GB
|
||||
|
||||
Other machines (≤ 1 min old): desktop offline · macbook offline · macmini ok: 26.6 GB free, 14 threads 69% busy, 1 sessions
|
||||
|
||||
# During the run, once a minute (uptime, then ~/fleet/load fleet)
|
||||
18:25:30
|
||||
18:25:30 up 8 days 3:08, 0 users, load average: 2.13, 1.53, 1.29
|
||||
ludothinkpad ok: 29.1 GB free, 4 threads 2% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.6 GB free, 14 threads 87% busy, 1 sessions
|
||||
18:26:34
|
||||
18:26:34 up 8 days 3:09, 0 users, load average: 1.70, 1.56, 1.31
|
||||
ludothinkpad ok: 28.5 GB free, 4 threads 23% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.4 GB free, 14 threads 85% busy, 1 sessions
|
||||
18:27:38
|
||||
18:27:38 up 8 days 3:10, 0 users, load average: 1.10, 1.43, 1.28
|
||||
ludothinkpad ok: 28.5 GB free, 4 threads 4% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.7 GB free, 14 threads 80% busy, 1 sessions
|
||||
18:28:42
|
||||
18:28:42 up 8 days 3:11, 0 users, load average: 0.93, 1.29, 1.24
|
||||
ludothinkpad ok: 28.5 GB free, 4 threads 22% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.0 GB free, 14 threads 82% busy, 1 sessions
|
||||
18:29:46
|
||||
18:29:46 up 8 days 3:12, 0 users, load average: 0.62, 1.12, 1.18
|
||||
ludothinkpad ok: 28.5 GB free, 4 threads 12% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 25.7 GB free, 14 threads 81% busy, 1 sessions
|
||||
18:30:50
|
||||
18:30:51 up 8 days 3:13, 0 users, load average: 0.79, 1.05, 1.15
|
||||
ludothinkpad ok: 28.5 GB free, 4 threads 7% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.0 GB free, 14 threads 88% busy, 1 sessions
|
||||
18:31:55
|
||||
18:31:55 up 8 days 3:14, 0 users, load average: 0.67, 0.96, 1.11
|
||||
ludothinkpad ok: 28.6 GB free, 4 threads 73% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.3 GB free, 14 threads 79% busy, 1 sessions
|
||||
18:32:59
|
||||
18:32:59 up 8 days 3:15, 0 users, load average: 4.01, 1.91, 1.44
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 24% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.4 GB free, 14 threads 85% busy, 1 sessions
|
||||
18:34:03
|
||||
18:34:03 up 8 days 3:16, 0 users, load average: 2.24, 1.84, 1.44
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 21% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.2 GB free, 14 threads 86% busy, 1 sessions
|
||||
18:35:07
|
||||
18:35:07 up 8 days 3:17, 0 users, load average: 1.28, 1.63, 1.40
|
||||
ludothinkpad ok: 25.8 GB free, 4 threads 21% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.0 GB free, 14 threads 86% busy, 1 sessions
|
||||
18:36:11
|
||||
18:36:11 up 8 days 3:18, 0 users, load average: 0.79, 1.41, 1.33
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.3 GB free, 14 threads 93% busy, 1 sessions
|
||||
18:37:15
|
||||
18:37:15 up 8 days 3:20, 0 users, load average: 1.44, 1.47, 1.36
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 23% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.8 GB free, 14 threads 86% busy, 1 sessions
|
||||
18:38:19
|
||||
18:38:19 up 8 days 3:21, 0 users, load average: 1.37, 1.45, 1.36
|
||||
ludothinkpad ok: 25.9 GB free, 4 threads 13% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 86% busy, 1 sessions
|
||||
18:39:24
|
||||
18:39:24 up 8 days 3:22, 0 users, load average: 2.27, 1.66, 1.44
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 62% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 70% busy, 1 sessions
|
||||
18:40:28
|
||||
18:40:28 up 8 days 3:23, 0 users, load average: 1.95, 1.72, 1.48
|
||||
ludothinkpad ok: 28.3 GB free, 4 threads 63% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.7 GB free, 14 threads 84% busy, 1 sessions
|
||||
18:41:32
|
||||
18:41:32 up 8 days 3:24, 0 users, load average: 3.32, 2.22, 1.67
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 61% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.8 GB free, 14 threads 85% busy, 1 sessions
|
||||
18:42:36
|
||||
18:42:36 up 8 days 3:25, 0 users, load average: 3.13, 2.33, 1.75
|
||||
ludothinkpad ok: 28.2 GB free, 4 threads 60% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
|
||||
18:43:40
|
||||
18:43:40 up 8 days 3:26, 0 users, load average: 3.27, 2.48, 1.83
|
||||
ludothinkpad busy: 28.1 GB free, 4 threads 90% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.3 GB free, 14 threads 92% busy, 1 sessions
|
||||
18:44:44
|
||||
18:44:44 up 8 days 3:27, 0 users, load average: 2.14, 2.32, 1.82
|
||||
ludothinkpad ok: 28.1 GB free, 4 threads 55% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 30.4 GB free, 14 threads 88% busy, 1 sessions
|
||||
18:45:48
|
||||
18:45:48 up 8 days 3:28, 0 users, load average: 4.94, 3.06, 2.11
|
||||
ludothinkpad busy: 25.9 GB free, 4 threads 86% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.7 GB free, 14 threads 84% busy, 1 sessions
|
||||
18:46:53
|
||||
18:46:53 up 8 days 3:29, 0 users, load average: 6.55, 3.92, 2.47
|
||||
ludothinkpad busy: 25.4 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.3 GB free, 14 threads 81% busy, 1 sessions
|
||||
18:47:57
|
||||
18:47:57 up 8 days 3:30, 0 users, load average: 8.50, 4.96, 2.93
|
||||
ludothinkpad busy: 24.0 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 27.9 GB free, 14 threads 90% busy, 1 sessions
|
||||
18:49:01
|
||||
18:49:01 up 8 days 3:31, 0 users, load average: 8.59, 5.68, 3.31
|
||||
ludothinkpad busy: 22.7 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.2 GB free, 14 threads 85% busy, 1 sessions
|
||||
18:50:05
|
||||
18:50:05 up 8 days 3:32, 0 users, load average: 7.81, 6.07, 3.61
|
||||
ludothinkpad busy: 24.5 GB free, 4 threads 90% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.6 GB free, 14 threads 96% busy, 1 sessions
|
||||
18:51:09
|
||||
18:51:09 up 8 days 3:33, 0 users, load average: 8.29, 6.50, 3.93
|
||||
ludothinkpad busy: 24.3 GB free, 4 threads 92% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.0 GB free, 14 threads 94% busy, 1 sessions
|
||||
18:52:14
|
||||
18:52:14 up 8 days 3:35, 0 users, load average: 5.58, 6.06, 3.95
|
||||
ludothinkpad busy: 26.0 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.3 GB free, 14 threads 87% busy, 1 sessions
|
||||
18:53:18
|
||||
18:53:18 up 8 days 3:36, 0 users, load average: 4.53, 5.69, 3.97
|
||||
ludothinkpad busy: 27.2 GB free, 4 threads 89% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.8 GB free, 14 threads 87% busy, 1 sessions
|
||||
18:54:22
|
||||
18:54:22 up 8 days 3:37, 0 users, load average: 5.31, 5.64, 4.07
|
||||
ludothinkpad busy: 26.3 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.3 GB free, 14 threads 91% busy, 1 sessions
|
||||
18:55:26
|
||||
18:55:26 up 8 days 3:38, 0 users, load average: 10.47, 6.99, 4.63
|
||||
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.7 GB free, 14 threads 90% busy, 1 sessions
|
||||
18:56:30
|
||||
18:56:30 up 8 days 3:39, 0 users, load average: 9.27, 7.45, 4.96
|
||||
ludothinkpad busy: 26.2 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.5 GB free, 14 threads 92% busy, 1 sessions
|
||||
18:57:35
|
||||
18:57:35 up 8 days 3:40, 0 users, load average: 7.79, 7.39, 5.10
|
||||
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.1 GB free, 14 threads 80% busy, 1 sessions
|
||||
18:58:39
|
||||
18:58:39 up 8 days 3:41, 0 users, load average: 8.06, 7.55, 5.32
|
||||
ludothinkpad busy: 26.4 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.8 GB free, 14 threads 95% busy, 1 sessions
|
||||
18:59:43
|
||||
18:59:43 up 8 days 3:42, 0 users, load average: 8.44, 7.65, 5.50
|
||||
ludothinkpad busy: 26.2 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.4 GB free, 14 threads 94% busy, 1 sessions
|
||||
19:00:47
|
||||
19:00:47 up 8 days 3:43, 0 users, load average: 11.04, 8.66, 6.00
|
||||
ludothinkpad busy: 24.9 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 31.8 GB free, 14 threads 88% busy, 1 sessions
|
||||
19:01:51
|
||||
19:01:51 up 8 days 3:44, 0 users, load average: 11.91, 9.37, 6.41
|
||||
ludothinkpad busy: 24.4 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 32.9 GB free, 14 threads 77% busy, 1 sessions
|
||||
19:02:56
|
||||
19:02:56 up 8 days 3:45, 0 users, load average: 11.64, 9.85, 6.79
|
||||
ludothinkpad ok: 25.5 GB free, 4 threads 70% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 30.9 GB free, 14 threads 91% busy, 1 sessions
|
||||
19:04:00
|
||||
19:04:00 up 8 days 3:46, 0 users, load average: 11.42, 10.06, 7.06
|
||||
ludothinkpad busy: 23.4 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.4 GB free, 14 threads 94% busy, 1 sessions
|
||||
19:05:04
|
||||
19:05:04 up 8 days 3:47, 0 users, load average: 9.83, 9.96, 7.24
|
||||
ludothinkpad busy: 23.2 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 31.4 GB free, 14 threads 80% busy, 1 sessions
|
||||
19:06:09
|
||||
19:06:09 up 8 days 3:48, 0 users, load average: 8.38, 9.68, 7.34
|
||||
ludothinkpad ok: 22.7 GB free, 4 threads 17% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.8 GB free, 14 threads 86% busy, 1 sessions
|
||||
19:07:13
|
||||
19:07:13 up 8 days 3:49, 0 users, load average: 4.51, 8.27, 7.00
|
||||
ludothinkpad ok: 25.7 GB free, 4 threads 75% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 26.5 GB free, 14 threads 89% busy, 1 sessions
|
||||
19:08:17
|
||||
19:08:17 up 8 days 3:51, 0 users, load average: 6.77, 8.13, 7.04
|
||||
ludothinkpad busy: 25.8 GB free, 4 threads 95% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 30.8 GB free, 14 threads 91% busy, 1 sessions
|
||||
19:09:22
|
||||
19:09:22 up 8 days 3:52, 0 users, load average: 8.27, 8.25, 7.14
|
||||
ludothinkpad busy: 25.2 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.3 GB free, 14 threads 91% busy, 1 sessions
|
||||
19:10:26
|
||||
19:10:26 up 8 days 3:53, 0 users, load average: 8.88, 8.57, 7.34
|
||||
ludothinkpad busy: 24.8 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 32.4 GB free, 14 threads 72% busy, 1 sessions
|
||||
19:11:30
|
||||
19:11:30 up 8 days 3:54, 0 users, load average: 13.33, 10.05, 7.94
|
||||
ludothinkpad busy: 24.7 GB free, 4 threads 97% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 30.1 GB free, 14 threads 87% busy, 1 sessions
|
||||
19:12:35
|
||||
19:12:35 up 8 days 3:55, 0 users, load average: 10.92, 10.13, 8.12
|
||||
ludothinkpad ok: 26.4 GB free, 4 threads 76% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.3 GB free, 14 threads 86% busy, 1 sessions
|
||||
19:13:39
|
||||
19:13:39 up 8 days 3:56, 0 users, load average: 9.98, 9.89, 8.16
|
||||
ludothinkpad busy: 25.8 GB free, 4 threads 90% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.8 GB free, 14 threads 93% busy, 1 sessions
|
||||
19:14:43
|
||||
19:14:43 up 8 days 3:57, 0 users, load average: 8.95, 9.63, 8.19
|
||||
ludothinkpad busy: 25.1 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 29.6 GB free, 14 threads 71% busy, 1 sessions
|
||||
19:15:47
|
||||
19:15:47 up 8 days 3:58, 0 users, load average: 12.02, 10.38, 8.55
|
||||
ludothinkpad busy: 24.5 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 28.1 GB free, 14 threads 93% busy, 1 sessions
|
||||
19:16:52
|
||||
19:16:52 up 8 days 3:59, 0 users, load average: 13.82, 11.21, 8.95
|
||||
ludothinkpad busy: 23.9 GB free, 4 threads 100% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.4 GB free, 14 threads 75% busy, 1 sessions
|
||||
19:17:56
|
||||
19:17:56 up 8 days 4:00, 0 users, load average: 11.95, 11.20, 9.10
|
||||
ludothinkpad busy: 25.0 GB free, 4 threads 99% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.4 GB free, 14 threads 81% busy, 1 sessions
|
||||
19:19:00
|
||||
19:19:00 up 8 days 4:01, 0 users, load average: 11.46, 11.14, 9.22
|
||||
ludothinkpad busy: 24.6 GB free, 4 threads 95% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.4 GB free, 14 threads 83% busy, 1 sessions
|
||||
19:20:04
|
||||
19:20:04 up 8 days 4:02, 0 users, load average: 11.11, 10.97, 9.28
|
||||
ludothinkpad busy: 24.6 GB free, 4 threads 93% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 26.7 GB free, 14 threads 85% busy, 1 sessions
|
||||
19:21:08
|
||||
19:21:08 up 8 days 4:03, 0 users, load average: 9.81, 10.60, 9.27
|
||||
ludothinkpad busy: 24.4 GB free, 4 threads 91% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 27.8 GB free, 14 threads 75% busy, 1 sessions
|
||||
19:22:13
|
||||
19:22:13 up 8 days 4:04, 0 users, load average: 5.98, 9.51, 9.00
|
||||
ludothinkpad ok: 23.8 GB free, 4 threads 18% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini busy: 29.7 GB free, 14 threads 85% busy, 1 sessions
|
||||
19:23:17
|
||||
19:23:17 up 8 days 4:06, 0 users, load average: 4.01, 8.27, 8.60
|
||||
ludothinkpad ok: 23.1 GB free, 4 threads 56% busy, 0 sessions
|
||||
desktop offline
|
||||
macmini ok: 28.2 GB free, 14 threads 79% busy, 1 sessions
|
||||
@@ -1,367 +0,0 @@
|
||||
{
|
||||
"kind": "horizon-server-benchmark",
|
||||
"date": "2026-10-18T17:23:30+00:00",
|
||||
"machine": {
|
||||
"system": "Linux",
|
||||
"machine": "x86_64",
|
||||
"cpus": 4,
|
||||
"note": "ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS, Linux 6.18; the bots on the Mac mini (M4 Pro) over the tailnet"
|
||||
},
|
||||
"commit": "4ea2243e4d1a44f910857bd759845db70e3690a8",
|
||||
"java": [
|
||||
"openjdk 25.0.4.1 2026-08-18"
|
||||
],
|
||||
"settings": {
|
||||
"servers": "horizon,vanilla",
|
||||
"bots": "1,10,25,50",
|
||||
"behaviour": "mixed",
|
||||
"join_every": 1.0,
|
||||
"warmup": 60,
|
||||
"seconds": 300,
|
||||
"seed": 12345,
|
||||
"explore_speed": 0,
|
||||
"gamerule": "",
|
||||
"heap": "4g",
|
||||
"out": "mixed.json",
|
||||
"nice": 19,
|
||||
"bots_via": "ssh macmini",
|
||||
"bots_bin": "horizon-bots (on the Mac mini)",
|
||||
"connect": "<the ThinkPad's tailnet address>",
|
||||
"properties": {
|
||||
"level-seed": "12345",
|
||||
"view-distance": "10",
|
||||
"simulation-distance": "10",
|
||||
"online-mode": "false",
|
||||
"white-list": "false",
|
||||
"gamemode": "creative",
|
||||
"force-gamemode": "true",
|
||||
"spawn-protection": "0",
|
||||
"max-players": "200",
|
||||
"difficulty": "easy",
|
||||
"sync-chunk-writes": "true"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 1,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 10.92,
|
||||
"mspt": {
|
||||
"average": 2.38,
|
||||
"p50": 1.97,
|
||||
"p95": 5.17,
|
||||
"p99": 18.2
|
||||
},
|
||||
"mspt_worst_p99": 45.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 653.7,
|
||||
"max": 659.2
|
||||
},
|
||||
"cpu_percent": 24.51,
|
||||
"chunks_per_second": 9.9,
|
||||
"unique_per_second": 0.3,
|
||||
"forgotten_per_second": 9.93,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3951,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3951.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 1,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 14.75,
|
||||
"mspt": {
|
||||
"average": 17.9,
|
||||
"p50": 18.39,
|
||||
"p95": 27.58,
|
||||
"p99": 47.28
|
||||
},
|
||||
"mspt_worst_p99": 423.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3382.06,
|
||||
"max": 3394.1
|
||||
},
|
||||
"cpu_percent": 64.02,
|
||||
"chunks_per_second": 9.92,
|
||||
"unique_per_second": 0.29,
|
||||
"forgotten_per_second": 9.95,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 3846,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3846.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 699.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 0,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 1,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 10,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 10.22,
|
||||
"mspt": {
|
||||
"average": 12.39,
|
||||
"p50": 8.33,
|
||||
"p95": 35.99,
|
||||
"p99": 65.9
|
||||
},
|
||||
"mspt_worst_p99": 117.1,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 935.85,
|
||||
"max": 1008.6
|
||||
},
|
||||
"cpu_percent": 210.37,
|
||||
"chunks_per_second": 98.22,
|
||||
"unique_per_second": 36.49,
|
||||
"forgotten_per_second": 98.25,
|
||||
"waited_ticks_per_second": 0.0,
|
||||
"chunks_total": 39618,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3992.17,
|
||||
"explore": 7359.5,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 696.83,
|
||||
"explore": 6869.0,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 6267,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 10,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 14.54,
|
||||
"mspt": {
|
||||
"average": 34.47,
|
||||
"p50": 29.67,
|
||||
"p95": 63.52,
|
||||
"p99": 138.58
|
||||
},
|
||||
"mspt_worst_p99": 884.7,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3594.26,
|
||||
"max": 3661.7
|
||||
},
|
||||
"cpu_percent": 217.13,
|
||||
"chunks_per_second": 24.34,
|
||||
"unique_per_second": 19.13,
|
||||
"forgotten_per_second": 20.12,
|
||||
"waited_ticks_per_second": 0.62,
|
||||
"chunks_total": 10443,
|
||||
"chunks_per_bot": {
|
||||
"walk": 679.83,
|
||||
"explore": 2824.0,
|
||||
"idle": 358.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 389.17,
|
||||
"explore": 2824.0,
|
||||
"idle": 358.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 5500,
|
||||
"moved_too_quickly": 12,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 10,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 14.73,
|
||||
"mspt": {
|
||||
"average": 24.0,
|
||||
"p50": 21.07,
|
||||
"p95": 49.72,
|
||||
"p99": 80.57
|
||||
},
|
||||
"mspt_worst_p99": 168.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1013.07,
|
||||
"max": 1179.4
|
||||
},
|
||||
"cpu_percent": 192.47,
|
||||
"chunks_per_second": 178.23,
|
||||
"unique_per_second": 31.85,
|
||||
"forgotten_per_second": 177.93,
|
||||
"waited_ticks_per_second": 60.59,
|
||||
"chunks_total": 73657,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3927.87,
|
||||
"explore": 2474.8,
|
||||
"idle": 473.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 691.73,
|
||||
"explore": 2447.8,
|
||||
"idle": 473.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 5532,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 58
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 25,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 28.57,
|
||||
"mspt": {
|
||||
"average": 31.59,
|
||||
"p50": 25.18,
|
||||
"p95": 70.03,
|
||||
"p99": 222.74
|
||||
},
|
||||
"mspt_worst_p99": 2319.6,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3742.51,
|
||||
"max": 4055.0
|
||||
},
|
||||
"cpu_percent": 218.34,
|
||||
"chunks_per_second": 30.37,
|
||||
"unique_per_second": 10.84,
|
||||
"forgotten_per_second": 29.44,
|
||||
"waited_ticks_per_second": 50.91,
|
||||
"chunks_total": 14193,
|
||||
"chunks_per_bot": {
|
||||
"walk": 640.2,
|
||||
"explore": 738.0,
|
||||
"idle": 180.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 211.33,
|
||||
"explore": 737.6,
|
||||
"idle": 180.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 4524,
|
||||
"moved_too_quickly": 324,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 25,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 56
|
||||
},
|
||||
{
|
||||
"server": "horizon",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 19.84,
|
||||
"mspt": {
|
||||
"average": 35.2,
|
||||
"p50": 32.12,
|
||||
"p95": 69.04,
|
||||
"p99": 96.56
|
||||
},
|
||||
"mspt_worst_p99": 240.8,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 1013.2,
|
||||
"max": 1249.1
|
||||
},
|
||||
"cpu_percent": 217.68,
|
||||
"chunks_per_second": 273.18,
|
||||
"unique_per_second": 34.58,
|
||||
"forgotten_per_second": 268.19,
|
||||
"waited_ticks_per_second": 138.29,
|
||||
"chunks_total": 115527,
|
||||
"chunks_per_bot": {
|
||||
"walk": 3300.1,
|
||||
"explore": 1192.4,
|
||||
"idle": 460.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 632.67,
|
||||
"explore": 1186.9,
|
||||
"idle": 460.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 8369,
|
||||
"moved_too_quickly": 0,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 57
|
||||
},
|
||||
{
|
||||
"server": "vanilla",
|
||||
"bots": 50,
|
||||
"behaviour": "mixed",
|
||||
"start_seconds": 20.58,
|
||||
"mspt": {
|
||||
"average": 29.74,
|
||||
"p50": 23.6,
|
||||
"p95": 62.25,
|
||||
"p99": 137.41
|
||||
},
|
||||
"mspt_worst_p99": 489.3,
|
||||
"tps": 20.0,
|
||||
"rss_mb": {
|
||||
"mean": 3995.71,
|
||||
"max": 4597.8
|
||||
},
|
||||
"cpu_percent": 294.31,
|
||||
"chunks_per_second": 94.36,
|
||||
"unique_per_second": 17.76,
|
||||
"forgotten_per_second": 94.69,
|
||||
"waited_ticks_per_second": 101.48,
|
||||
"chunks_total": 41637,
|
||||
"chunks_per_bot": {
|
||||
"walk": 1104.97,
|
||||
"explore": 628.8,
|
||||
"idle": 220.0
|
||||
},
|
||||
"unique_chunks_per_bot": {
|
||||
"walk": 279.4,
|
||||
"explore": 626.4,
|
||||
"idle": 220.0
|
||||
},
|
||||
"resent": 0,
|
||||
"pulled_back": 12034,
|
||||
"moved_too_quickly": 2193,
|
||||
"moved_wrongly": 0,
|
||||
"bots_joined": 50,
|
||||
"bots_failed": [],
|
||||
"tick_queries": 57
|
||||
}
|
||||
],
|
||||
"horizon_build": "v2026.10.18.15 release binaries (horizon-client, the engine, at 6c078b28); horizon-server from #898 (4ea2243e, the supervisor only)"
|
||||
}
|
||||
@@ -4,10 +4,10 @@ version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version = "1.86"
|
||||
description = "cargo horizon-mod new/build/run/check: make Horizon mods in Rust (docs/mods.md)"
|
||||
description = "cargo horizon-mod new/build/run: make Horizon mods in Rust (docs/mods.md)"
|
||||
|
||||
[dependencies]
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack", "runtime", "index"] }
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack"] }
|
||||
serde_json = "1.0.151"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
//! `cargo horizon-mod check <file>`: what the registry checks of a submission,
|
||||
//! run anywhere: its kind's own checks (docs/api/mod-index.md), its signature
|
||||
//! when given a key, and for a mod a trial run in the sandbox: it must start,
|
||||
//! survive ticks, frames, a chat line and each of its commands (a server mod:
|
||||
//! a player joining, chatting, typing them and leaving), and import no more
|
||||
//! than its capabilities.
|
||||
use horizon_mods::{
|
||||
Clock, GameMode, Mods, Output, Package, Player, ServerPlayer, Snapshot, Status,
|
||||
index::{self, Facts, Kind},
|
||||
manifest::component_imports,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::Path;
|
||||
|
||||
/// How long the trial run plays: two seconds of game.
|
||||
const TICKS: u64 = 40;
|
||||
const FRAMES_PER_TICK: u64 = 3;
|
||||
|
||||
pub struct Report {
|
||||
pub kind: Kind,
|
||||
pub facts: Facts,
|
||||
pub sha256: String,
|
||||
pub size: u64,
|
||||
pub signed: bool,
|
||||
/// A mod has code to run (a content mod may have none).
|
||||
pub code: bool,
|
||||
/// A mod's component imports.
|
||||
pub imports: Vec<String>,
|
||||
/// A mod's commands, each run once.
|
||||
pub commands: Vec<String>,
|
||||
/// What the mod showed during the trial run.
|
||||
pub output: Vec<String>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn json(&self) -> Value {
|
||||
let requires: Vec<Value> = self
|
||||
.facts
|
||||
.requires
|
||||
.iter()
|
||||
.map(|r| json!({"id": r.id, "version": r.version, "sha256": r.sha256}))
|
||||
.collect();
|
||||
json!({
|
||||
"kind": self.kind.name(),
|
||||
"id": self.facts.id,
|
||||
"version": self.facts.version,
|
||||
"api": self.facts.api,
|
||||
"side": self.facts.side,
|
||||
"capabilities": self.facts.capabilities,
|
||||
"api_level": self.facts.api_level,
|
||||
"game": self.facts.game,
|
||||
"requires": requires,
|
||||
"sha256": self.sha256,
|
||||
"size": self.size,
|
||||
"signed": self.signed,
|
||||
"code": self.code,
|
||||
"imports": self.imports,
|
||||
"commands": self.commands,
|
||||
"output": self.output,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
let f = &self.facts;
|
||||
let mut out = format!(
|
||||
"{} {} ({}",
|
||||
f.id.as_deref().unwrap_or("resource pack"),
|
||||
f.version.as_deref().unwrap_or(""),
|
||||
self.kind.name()
|
||||
);
|
||||
if let Some(side) = f.side {
|
||||
let side = serde_json::to_value(side).unwrap_or_default();
|
||||
out += &format!(", API {}, {}", f.api, side.as_str().unwrap_or("?"));
|
||||
} else if self.kind == Kind::Shader {
|
||||
out += &format!(", shader API {}", f.api);
|
||||
} else if !f.api.is_empty() {
|
||||
out += &format!(", format {}", f.api);
|
||||
}
|
||||
if let Some(game) = &f.game {
|
||||
out += &format!(", for {game}");
|
||||
}
|
||||
let signed = if self.signed {
|
||||
"signature verified"
|
||||
} else {
|
||||
"unsigned"
|
||||
};
|
||||
out += &format!(
|
||||
")\n {} bytes, SHA-256 {}, {signed}\n",
|
||||
self.size, self.sha256
|
||||
);
|
||||
if let Some(level) = f.api_level {
|
||||
out += &format!(" needs mod API level {level} or newer\n");
|
||||
}
|
||||
for c in &f.capabilities {
|
||||
out += &format!(" may {}\n", c.describe());
|
||||
}
|
||||
for r in &f.requires {
|
||||
out += &format!(" needs {} {} ({})\n", r.id, r.version, &r.sha256[..12]);
|
||||
}
|
||||
if self.kind == Kind::Mod && !self.code {
|
||||
out += " no code: content only, nothing to run\n";
|
||||
} else if self.kind == Kind::Mod {
|
||||
let commands = if self.commands.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(", /{}", self.commands.join(", /"))
|
||||
};
|
||||
out += &format!(
|
||||
" trial run: started, {TICKS} ticks, {} frames, a chat line{commands}: ok\n",
|
||||
TICKS * FRAMES_PER_TICK
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind a file's extension says.
|
||||
pub fn kind_of(path: &Path) -> Option<Kind> {
|
||||
match path.extension()?.to_str()? {
|
||||
"hmod" => Some(Kind::Mod),
|
||||
"hpack" => Some(Kind::Modpack),
|
||||
"zip" => Some(Kind::ResourcePack),
|
||||
"hshader" => Some(Kind::Shader),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the file at `path` as `kind`; `key` and `signature` (the
|
||||
/// `.minisig` text) check its signature.
|
||||
pub fn check(
|
||||
path: &Path,
|
||||
kind: Kind,
|
||||
key: Option<&str>,
|
||||
signature: Option<&str>,
|
||||
) -> Result<Report, String> {
|
||||
let size = std::fs::metadata(path)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?
|
||||
.len();
|
||||
if size > index::MAX_FILE {
|
||||
return Err(format!("over {} MiB", index::MAX_FILE >> 20));
|
||||
}
|
||||
index::open(&kind)?;
|
||||
let facts = index::inspect(&kind, path)?;
|
||||
let signed = match (key, signature) {
|
||||
(Some(key), Some(sig)) => {
|
||||
index::verify_signature(key, sig, path)?;
|
||||
true
|
||||
}
|
||||
(None, None) => false,
|
||||
_ => return Err("--key and --signature go together".into()),
|
||||
};
|
||||
let mut report = Report {
|
||||
kind,
|
||||
facts,
|
||||
sha256: index::sha256_file(path)?,
|
||||
size,
|
||||
signed,
|
||||
code: false,
|
||||
imports: Vec::new(),
|
||||
commands: Vec::new(),
|
||||
output: Vec::new(),
|
||||
};
|
||||
if report.kind == Kind::Mod {
|
||||
trial_run(path, &mut report)?;
|
||||
}
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Runs a mod in the sandbox, as the game does, from a scratch copy so its
|
||||
/// storage lands in a folder removed afterwards. A content mod has no code to
|
||||
/// run: `inspect` already read its content as the game does.
|
||||
fn trial_run(path: &Path, report: &mut Report) -> Result<(), String> {
|
||||
let package = Package::read(path)?;
|
||||
let Some(wasm) = package.wasm.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
report.code = true;
|
||||
report.imports = component_imports(wasm)?;
|
||||
package
|
||||
.manifest
|
||||
.check_imports(report.imports.iter().map(String::as_str))?;
|
||||
let dir = std::env::temp_dir().join(format!("horizon-mod-check-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let copy = dir.join("mod.hmod");
|
||||
let result = std::fs::copy(path, ©)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|_| play(©, report));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
result
|
||||
}
|
||||
|
||||
fn play(path: &Path, report: &mut Report) -> Result<(), String> {
|
||||
let mut mods = Mods::load(&[path.to_path_buf()], |_| true, |_| false);
|
||||
let failed = |mods: &Mods| match &mods.entries()[0].status {
|
||||
Status::Failed(reason) => Some(reason.clone()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(reason) = failed(&mods) {
|
||||
return Err(format!("it didn't start: {reason}"));
|
||||
}
|
||||
let client: Vec<String> = mods.commands().map(str::to_owned).collect();
|
||||
let server: Vec<String> = mods.server_commands().map(|c| c.name.clone()).collect();
|
||||
report.commands = [client.clone(), server.clone()].concat();
|
||||
// A server mod plays with one player on, who joins, chats and leaves.
|
||||
let steve = ServerPlayer {
|
||||
id: "00000000-0000-4000-8000-000000000001".into(),
|
||||
name: "Steve".into(),
|
||||
x: 0.5,
|
||||
y: 64.0,
|
||||
z: 0.5,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
game_mode: GameMode::Survival,
|
||||
health: 20.0,
|
||||
};
|
||||
let snapshot = |t: u64| Snapshot {
|
||||
player: Some(Player {
|
||||
x: 0.5 + t as f64 * 0.1,
|
||||
y: 64.0,
|
||||
z: 0.5,
|
||||
yaw: 90.0,
|
||||
pitch: 0.0,
|
||||
health: 20.0,
|
||||
food: 20,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
}),
|
||||
clock: Some(Clock {
|
||||
game_time: t,
|
||||
day_time: 1000 + t,
|
||||
}),
|
||||
players: vec![steve.clone()],
|
||||
// The GUI of a 1280 x 720 window at scale 3.
|
||||
gui_size: (427.0, 240.0),
|
||||
..Snapshot::default()
|
||||
};
|
||||
mods.set_snapshot(snapshot(0));
|
||||
mods.player_joined(&steve);
|
||||
for t in 0..TICKS {
|
||||
mods.set_snapshot(snapshot(t));
|
||||
mods.tick(t);
|
||||
mods.server_tick(t);
|
||||
for _ in 0..FRAMES_PER_TICK {
|
||||
mods.frame(1.0 / 60.0);
|
||||
}
|
||||
}
|
||||
mods.chat("<Steve> hello");
|
||||
mods.player_chat(&steve, "hello");
|
||||
for name in &client {
|
||||
mods.command(name);
|
||||
}
|
||||
for name in &server {
|
||||
mods.server_command(&steve, name);
|
||||
}
|
||||
mods.set_snapshot(Snapshot::default());
|
||||
mods.player_left(&steve);
|
||||
for out in mods.drain() {
|
||||
report.output.push(match out {
|
||||
Output::Chat { text, .. } => format!("chat: {text}"),
|
||||
Output::Tell { text, .. } => format!("tell: {text}"),
|
||||
Output::Broadcast { text, .. } => format!("broadcast: {text}"),
|
||||
Output::Feedback { text, error } | Output::Reply { text, error, .. } => {
|
||||
format!("{}: {text}", if error { "error" } else { "feedback" })
|
||||
}
|
||||
Output::Command { line } => format!("command: /{line}"),
|
||||
Output::QuietCommands { lines, .. } => {
|
||||
format!("{} quiet command(s)", lines.len())
|
||||
}
|
||||
Output::Stopped { reason, .. } => return Err(format!("it stopped: {reason}")),
|
||||
Output::SetBlocks {
|
||||
dimension, blocks, ..
|
||||
} => {
|
||||
format!("set {} block(s) in {dimension}", blocks.len())
|
||||
}
|
||||
Output::OpenScreen { .. } => "opened a screen".to_string(),
|
||||
Output::CloseScreen { .. } => "closed its screen".to_string(),
|
||||
Output::PlaceRecipe { book_id, all } => {
|
||||
format!("placed recipe {book_id}{}", if all { " (all)" } else { "" })
|
||||
}
|
||||
Output::Give { item, count, .. } => format!("gave {count} {item}"),
|
||||
Output::Shake { strength, seconds } => {
|
||||
format!("shook the camera: {strength:.2} for {seconds:.1} s")
|
||||
}
|
||||
});
|
||||
}
|
||||
let shapes = mods.hud_shapes().count();
|
||||
if shapes > 0 {
|
||||
report.output.push(format!("drew {shapes} HUD shape(s)"));
|
||||
}
|
||||
match failed(&mods) {
|
||||
Some(reason) => Err(format!("it stopped: {reason}")),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fixture(name: &str) -> std::path::PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../horizon-mods/tests/index")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_each_kind_its_own_way() {
|
||||
let pack = fixture("cozy-1.0.0.hpack");
|
||||
let report = check(&pack, kind_of(&pack).unwrap(), None, None).unwrap();
|
||||
assert_eq!(report.facts.requires[0].id, "coords_hud");
|
||||
assert!(
|
||||
report
|
||||
.text()
|
||||
.contains("cozy 1.0.0 (modpack, format 1, for 26.3)")
|
||||
);
|
||||
// The fixture mod's code is a stand-in: its manifest reads, its run fails.
|
||||
let hmod = fixture("coords_hud-0.1.0.hmod");
|
||||
let key = std::fs::read_to_string(fixture("test.pub")).unwrap();
|
||||
let sig = std::fs::read_to_string(fixture("coords_hud-0.1.0.hmod.minisig")).unwrap();
|
||||
let key = key.lines().last().unwrap();
|
||||
let e = check(&hmod, Kind::Mod, Some(key), Some(&sig))
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(e.contains("didn't start"), "{e}");
|
||||
let e = check(
|
||||
&hmod,
|
||||
Kind::Mod,
|
||||
Some(key),
|
||||
Some("untrusted comment: x\nRUQ=\n"),
|
||||
)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(e.contains("signature"), "{e}");
|
||||
let e = check(&pack, Kind::Shader, None, None).err().unwrap();
|
||||
assert!(e.contains("horizon-shaders.toml"), "{e}");
|
||||
assert!(check(&pack, Kind::Mod, None, None).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,13 @@
|
||||
//! `cargo horizon-mod`: make Horizon mods in Rust (`docs/mods.md`).
|
||||
//!
|
||||
//! - `new <path> [--template code|content] [--name "Name"] [--sdk-path <dir>]`:
|
||||
//! a mod from a template: a Rust crate, or a content mod (no code).
|
||||
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`, with the mod's
|
||||
//! `assets/` and `data/`; a folder without `Cargo.toml` is content only.
|
||||
//! - `new <path> [--name "Name"] [--sdk-path <dir>]`: a mod crate from the template.
|
||||
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`.
|
||||
//! - `run [--game <horizon-client>] [-- <game args>]`: build, then play with
|
||||
//! the mod; run `build` again and the game reloads it.
|
||||
//! - `test [--debug] [--hmod <file>] [<name>]`: build, then run the mod's
|
||||
//! `tests/*.toml` scripts in the sandbox, with no game.
|
||||
//! - `check <file> [--json]`: what the registry checks of a submission
|
||||
//! (`.hmod`, `.hpack`, resource pack `.zip`), with a trial run for a mod.
|
||||
//! - `registry build|review|check|list`: the registry repository's CI.
|
||||
use horizon_mods::{
|
||||
Capability, Content, Manifest, Package,
|
||||
Manifest, Package,
|
||||
manifest::{MANIFEST, component_imports, componentize},
|
||||
testing::{Script, TestGame},
|
||||
};
|
||||
mod check;
|
||||
mod registry;
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitCode, Stdio},
|
||||
@@ -39,10 +28,7 @@ fn main() -> ExitCode {
|
||||
println!("Built {}", p.display());
|
||||
}),
|
||||
Some("run") => run(&args[1..]),
|
||||
Some("test") => test(&args[1..]),
|
||||
Some("check") => check(&args[1..]),
|
||||
Some("registry") => registry::main(&args[1..]),
|
||||
_ => Err("usage: cargo horizon-mod new <path> [--template code|content] | build [--debug] | run [--game <horizon-client>] [-- <game args>] | test [--debug] [--hmod <file>] [<name>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
|
||||
_ => Err("usage: cargo horizon-mod new <path> | build [--debug] | run [--game <horizon-client>] [-- <game args>]".into()),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
@@ -96,72 +82,19 @@ fn new(args: &[String]) -> Result<(), String> {
|
||||
.replace("{name}", &name)
|
||||
.replace("{sdk}", &sdk)
|
||||
};
|
||||
let code = [
|
||||
let files = [
|
||||
("Cargo.toml", include_str!("../template/Cargo.toml.in")),
|
||||
(MANIFEST, include_str!("../template/horizon-mod.toml")),
|
||||
("src/lib.rs", include_str!("../template/src/lib.rs")),
|
||||
(".gitignore", include_str!("../template/gitignore")),
|
||||
(
|
||||
"tests/hello.toml",
|
||||
include_str!("../template/tests/hello.toml"),
|
||||
),
|
||||
];
|
||||
// A block copying a vanilla one, with its model, name, loot, recipe and tag.
|
||||
let content = [
|
||||
(
|
||||
MANIFEST,
|
||||
include_str!("../template/content/horizon-mod.toml"),
|
||||
),
|
||||
(
|
||||
"data/{id}/horizon/block/glowing_amethyst.json",
|
||||
include_str!("../template/content/block.json"),
|
||||
),
|
||||
(
|
||||
"assets/{id}/blockstates/glowing_amethyst.json",
|
||||
include_str!("../template/content/blockstate.json"),
|
||||
),
|
||||
(
|
||||
"assets/{id}/models/block/glowing_amethyst.json",
|
||||
include_str!("../template/content/model.json"),
|
||||
),
|
||||
(
|
||||
"assets/{id}/items/glowing_amethyst.json",
|
||||
include_str!("../template/content/item.json"),
|
||||
),
|
||||
(
|
||||
"assets/{id}/lang/en_us.json",
|
||||
include_str!("../template/content/lang.json"),
|
||||
),
|
||||
(
|
||||
"data/{id}/loot_table/blocks/glowing_amethyst.json",
|
||||
include_str!("../template/content/loot.json"),
|
||||
),
|
||||
(
|
||||
"data/{id}/recipe/glowing_amethyst.json",
|
||||
include_str!("../template/content/recipe.json"),
|
||||
),
|
||||
(
|
||||
"data/minecraft/tags/block/mineable/pickaxe.json",
|
||||
include_str!("../template/content/pickaxe.json"),
|
||||
),
|
||||
(".gitignore", include_str!("../template/gitignore")),
|
||||
];
|
||||
let files: &[(&str, &str)] = match flag(args, "--template").unwrap_or("code") {
|
||||
"code" => &code,
|
||||
"content" => &content,
|
||||
other => return Err(format!("no template {other}: code or content")),
|
||||
};
|
||||
for &(file, text) in files {
|
||||
let file = fill(file);
|
||||
let file = file.as_str();
|
||||
for (file, text) in files {
|
||||
let to = dir.join(file);
|
||||
std::fs::create_dir_all(to.parent().expect("a file in the mod"))
|
||||
.map_err(|e| e.to_string())?;
|
||||
std::fs::write(&to, fill(text)).map_err(|e| format!("{}: {e}", to.display()))?;
|
||||
}
|
||||
let manifest =
|
||||
Manifest::parse(&std::fs::read_to_string(dir.join(MANIFEST)).map_err(|e| e.to_string())?)?;
|
||||
Content::read(dir, &manifest)?;
|
||||
Manifest::parse(&std::fs::read_to_string(dir.join(MANIFEST)).map_err(|e| e.to_string())?)?;
|
||||
println!(
|
||||
"Created {} (mod id {id}). Next: cd {path} && cargo horizon-mod run",
|
||||
dir.display()
|
||||
@@ -169,53 +102,14 @@ fn new(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the mod in the current folder and packs `target/horizon-mod/<id>.hmod`:
|
||||
/// the manifest, the crate's component (none without a `Cargo.toml`: a
|
||||
/// content mod) and the files in `assets/` and `data/`.
|
||||
/// Builds the crate in the current folder and packs `target/horizon-mod/<id>.hmod`.
|
||||
fn build(release: bool) -> Result<PathBuf, String> {
|
||||
build_in(Path::new("."), release)
|
||||
}
|
||||
|
||||
fn build_in(root: &Path, release: bool) -> Result<PathBuf, String> {
|
||||
let manifest_text = std::fs::read_to_string(root.join(MANIFEST))
|
||||
let manifest_text = std::fs::read_to_string(MANIFEST)
|
||||
.map_err(|e| format!("{MANIFEST}: {e} (run this in a mod's folder)"))?;
|
||||
let manifest = Manifest::parse(&manifest_text)?;
|
||||
let (wasm, target_dir) = if root.join("Cargo.toml").exists() {
|
||||
let (wasm, target) = compile(root, release)?;
|
||||
// The game refuses a component importing capabilities the manifest lacks.
|
||||
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
|
||||
(Some(wasm), target)
|
||||
} else if manifest.capabilities.contains(&Capability::Registries) {
|
||||
(None, root.join("target"))
|
||||
} else {
|
||||
return Err(format!(
|
||||
"no Cargo.toml: only a content mod (capability registries in {MANIFEST}) has no code"
|
||||
));
|
||||
};
|
||||
// The same checks as the game's, and the files in the order it lists them.
|
||||
let content = Content::read(root, &manifest)?;
|
||||
let mut reader = content.reader()?;
|
||||
let mut files = Vec::with_capacity(content.files.len());
|
||||
for name in &content.files {
|
||||
files.push((name.as_str(), reader.read(name)?));
|
||||
}
|
||||
let files: Vec<(&str, &[u8])> = files.iter().map(|(n, b)| (*n, b.as_slice())).collect();
|
||||
let dir = target_dir.join("horizon-mod");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let path = dir.join(format!("{}.hmod", manifest.id));
|
||||
// Write then rename, so a running game never reads half a file.
|
||||
let partial = path.with_extension("hmod.partial");
|
||||
let bytes = Package::write_files(&manifest_text, wasm.as_deref(), &files)?;
|
||||
std::fs::write(&partial, bytes).map_err(|e| e.to_string())?;
|
||||
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Builds the crate for WebAssembly: its component, and Cargo's target folder.
|
||||
fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
|
||||
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
|
||||
let mut cmd = Command::new(cargo);
|
||||
cmd.current_dir(root).args([
|
||||
cmd.args([
|
||||
"build",
|
||||
"--lib",
|
||||
"--target",
|
||||
@@ -243,96 +137,17 @@ fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
|
||||
.rfind(|f| f.extension().is_some_and(|e| e == "wasm"))
|
||||
.ok_or("the build made no .wasm: is the crate a cdylib?")?;
|
||||
let wasm = componentize(&std::fs::read(&core).map_err(|e| e.to_string())?)?;
|
||||
let target = core.ancestors().nth(3).ok_or("unexpected target folder")?;
|
||||
Ok((wasm, target.to_path_buf()))
|
||||
}
|
||||
|
||||
/// `check <file>`: the registry's checks of a submission, printed; fails
|
||||
/// when one does.
|
||||
fn check(args: &[String]) -> Result<(), String> {
|
||||
let file = args
|
||||
.first()
|
||||
.filter(|a| !a.starts_with("--"))
|
||||
.ok_or("cargo horizon-mod check <file>")?;
|
||||
let path = Path::new(file);
|
||||
let kind = match flag(args, "--kind") {
|
||||
Some(k) => horizon_mods::index::Kind::from(k.to_string()),
|
||||
None => check::kind_of(path)
|
||||
.ok_or("pass --kind mod|modpack|resourcepack: the extension doesn't say")?,
|
||||
};
|
||||
let signature = flag(args, "--signature")
|
||||
.map(|p| std::fs::read_to_string(p).map_err(|e| format!("{p}: {e}")))
|
||||
.transpose()?;
|
||||
let report = check::check(path, kind, flag(args, "--key"), signature.as_deref());
|
||||
if args.iter().any(|a| a == "--json") {
|
||||
let json = match &report {
|
||||
Ok(r) => serde_json::json!({"ok": true, "report": r.json()}),
|
||||
Err(e) => serde_json::json!({"ok": false, "error": e}),
|
||||
};
|
||||
println!("{json}");
|
||||
}
|
||||
let report = report?;
|
||||
if !args.iter().any(|a| a == "--json") {
|
||||
print!("{}", report.text());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `test`: every `tests/*.toml` script (those whose name holds `<name>`)
|
||||
/// against the built mod, or the `.hmod` given with `--hmod`.
|
||||
fn test(args: &[String]) -> Result<(), String> {
|
||||
let hmod = match flag(args, "--hmod") {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => build(!args.iter().any(|a| a == "--debug"))?,
|
||||
};
|
||||
let filter = args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(i, a)| !a.starts_with("--") && (*i == 0 || args[i - 1] != "--hmod"))
|
||||
.map(|(_, a)| a.as_str());
|
||||
let mut scripts: Vec<PathBuf> = std::fs::read_dir("tests")
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"tests/: {e} (a mod's tests are tests/<name>.toml: docs/mods.md, \"Testing a mod\")"
|
||||
)
|
||||
})?
|
||||
.filter_map(|e| Some(e.ok()?.path()))
|
||||
.filter(|p| p.extension().is_some_and(|e| e == "toml"))
|
||||
.filter(|p| filter.is_none_or(|f| p.to_string_lossy().contains(f)))
|
||||
.collect();
|
||||
scripts.sort();
|
||||
if scripts.is_empty() {
|
||||
return Err("no tests/*.toml to run".into());
|
||||
}
|
||||
println!(
|
||||
"running {} scripts against {}",
|
||||
scripts.len(),
|
||||
hmod.display()
|
||||
);
|
||||
let mut failed = 0;
|
||||
for path in &scripts {
|
||||
let result = std::fs::read_to_string(path)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|text| Script::parse(&text))
|
||||
.and_then(|script| {
|
||||
let mut game = TestGame::load(&hmod)?;
|
||||
script.run(&mut game)?;
|
||||
Ok(script.steps.len())
|
||||
});
|
||||
match result {
|
||||
Ok(steps) => println!("test {} ... ok ({steps} steps)", path.display()),
|
||||
Err(e) => {
|
||||
failed += 1;
|
||||
println!("test {} ... FAILED\n {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
let passed = scripts.len() - failed;
|
||||
if failed > 0 {
|
||||
return Err(format!("{failed} failed, {passed} passed"));
|
||||
}
|
||||
println!("test result: ok. {passed} passed");
|
||||
Ok(())
|
||||
// The game refuses a component importing capabilities the manifest lacks.
|
||||
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
|
||||
let target_dir = core.ancestors().nth(3).ok_or("unexpected target folder")?;
|
||||
let dir = target_dir.join("horizon-mod");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let path = dir.join(format!("{}.hmod", manifest.id));
|
||||
// Write then rename, so a running game never reads half a file.
|
||||
let partial = path.with_extension("hmod.partial");
|
||||
std::fs::write(&partial, Package::write(&manifest_text, &wasm)?).map_err(|e| e.to_string())?;
|
||||
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn run(args: &[String]) -> Result<(), String> {
|
||||
@@ -377,57 +192,10 @@ mod tests {
|
||||
);
|
||||
let cargo = std::fs::read_to_string(path.join("Cargo.toml")).unwrap();
|
||||
assert!(cargo.contains("name = \"my-mod\"") && cargo.contains(SDK_GIT));
|
||||
let script = std::fs::read_to_string(path.join("tests/hello.toml")).unwrap();
|
||||
assert_eq!(Script::parse(&script).unwrap().steps.len(), 3);
|
||||
assert!(
|
||||
new(&[path.display().to_string()]).is_err(),
|
||||
"never overwrites"
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_content_template_builds_and_checks_with_no_code() {
|
||||
let dir = std::env::temp_dir().join(format!("horizon-mod-content-{}", std::process::id()));
|
||||
let path = dir.join("gems");
|
||||
let new_args = [
|
||||
path.display().to_string(),
|
||||
"--template".into(),
|
||||
"content".into(),
|
||||
];
|
||||
new(&new_args).unwrap();
|
||||
assert!(!path.join("Cargo.toml").exists());
|
||||
let hmod = build_in(&path, true).unwrap();
|
||||
assert_eq!(hmod, path.join("target/horizon-mod/gems.hmod"));
|
||||
let package = Package::read(&hmod).unwrap();
|
||||
assert!(package.wasm.is_none());
|
||||
let content = Content::read(&hmod, &package.manifest).unwrap();
|
||||
assert!(
|
||||
content
|
||||
.files
|
||||
.contains(&"data/gems/horizon/block/glowing_amethyst.json".into())
|
||||
);
|
||||
assert_eq!(content.files.len(), 8, "{:?}", content.files);
|
||||
let report = check::check(&hmod, check::kind_of(&hmod).unwrap(), None, None).unwrap();
|
||||
assert!(!report.code);
|
||||
assert!(
|
||||
report.text().contains("no code: content only"),
|
||||
"{}",
|
||||
report.text()
|
||||
);
|
||||
// Without registries, a mod needs code.
|
||||
let manifest = std::fs::read_to_string(path.join(MANIFEST)).unwrap();
|
||||
let manifest = manifest.replace("[\"registries\"]", "[]");
|
||||
std::fs::write(path.join(MANIFEST), manifest).unwrap();
|
||||
assert!(build_in(&path, true).unwrap_err().contains("no Cargo.toml"));
|
||||
assert!(
|
||||
new(&[
|
||||
dir.join("x").display().to_string(),
|
||||
"--template".into(),
|
||||
"lua".into()
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
//! `cargo horizon-mod registry build|review|check|list`: the registry repository's CI
|
||||
//! (`ludovic111/horizon-mods`, docs/api/mod-index.md). Network-free: the
|
||||
//! workflow downloads the files and publishes what this writes.
|
||||
use horizon_mods::index::{
|
||||
FILE_HOST,
|
||||
registry::{self, Sources},
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
/// Where the registry's site is published (its icons).
|
||||
const SITE: &str = "https://ludovic111.github.io/horizon-mods/";
|
||||
|
||||
const USAGE: &str = "usage: cargo horizon-mod registry build <checkout> --files <dir> --out <index.json> [--release <url>] [--site <url>] [--generated <time>]
|
||||
cargo horizon-mod registry review <base checkout> <head checkout> --author <login> [--json]
|
||||
cargo horizon-mod registry check <base checkout> <head checkout> --files <dir>
|
||||
cargo horizon-mod registry list <checkout>";
|
||||
|
||||
fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
|
||||
super::flag(args, name)
|
||||
}
|
||||
|
||||
fn positional(args: &[String]) -> Vec<&str> {
|
||||
let mut out = Vec::new();
|
||||
let mut skip = false;
|
||||
for a in args {
|
||||
if skip {
|
||||
skip = false;
|
||||
} else if a == "--json" {
|
||||
} else if a.starts_with("--") {
|
||||
skip = true;
|
||||
} else {
|
||||
out.push(a.as_str());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn lines(errors: Vec<String>) -> String {
|
||||
format!("{} problem(s):\n {}", errors.len(), errors.join("\n "))
|
||||
}
|
||||
|
||||
pub fn main(args: &[String]) -> Result<(), String> {
|
||||
let rest = args.get(1..).unwrap_or_default();
|
||||
let pos = positional(rest);
|
||||
match args.first().map(String::as_str) {
|
||||
Some("build") => {
|
||||
let [checkout] = pos[..] else {
|
||||
return Err(USAGE.into());
|
||||
};
|
||||
let (files, out) = (flag(rest, "--files"), flag(rest, "--out"));
|
||||
let (Some(files), Some(out)) = (files, out) else {
|
||||
return Err(USAGE.into());
|
||||
};
|
||||
let generated = flag(rest, "--generated")
|
||||
.map(str::to_owned)
|
||||
.or_else(|| std::env::var("REGISTRY_GENERATED").ok())
|
||||
.unwrap_or_default();
|
||||
let index = registry::build(&Sources {
|
||||
registry: Path::new(checkout),
|
||||
files: Path::new(files),
|
||||
release: flag(rest, "--release").unwrap_or(FILE_HOST),
|
||||
site: flag(rest, "--site").unwrap_or(SITE),
|
||||
generated,
|
||||
})
|
||||
.map_err(lines)?;
|
||||
let text = serde_json::to_string_pretty(&index).map_err(|e| e.to_string())?;
|
||||
std::fs::write(out, text + "\n").map_err(|e| format!("{out}: {e}"))?;
|
||||
println!(
|
||||
"Wrote {out}: {} project(s), {} taken down",
|
||||
index.projects.len(),
|
||||
index.revoked.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Some("review") => {
|
||||
let [base, head] = pos[..] else {
|
||||
return Err(USAGE.into());
|
||||
};
|
||||
let author = flag(rest, "--author").ok_or(USAGE)?;
|
||||
let (base, _) = registry::load(Path::new(base)).map_err(lines)?;
|
||||
let (head, _) = registry::load(Path::new(head)).map_err(lines)?;
|
||||
let review = registry::review(&base, &head, author);
|
||||
if rest.iter().any(|a| a == "--json") {
|
||||
let json = serde_json::json!({"errors": review.errors, "notes": review.notes});
|
||||
println!("{json}");
|
||||
} else {
|
||||
for n in &review.notes {
|
||||
println!("note: {n}");
|
||||
}
|
||||
for e in &review.errors {
|
||||
println!("error: {e}");
|
||||
}
|
||||
}
|
||||
if review.errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"{} change(s) the registry refuses",
|
||||
review.errors.len()
|
||||
))
|
||||
}
|
||||
}
|
||||
// `check` (and its trial run) on each file `head` lists and `base`
|
||||
// doesn't: a pull request's new files.
|
||||
Some("check") => {
|
||||
let [base, head] = pos[..] else {
|
||||
return Err(USAGE.into());
|
||||
};
|
||||
let files = Path::new(flag(rest, "--files").ok_or(USAGE)?);
|
||||
let old: Vec<String> = match registry::load(Path::new(base)) {
|
||||
Ok((entries, _)) => entries
|
||||
.iter()
|
||||
.flat_map(|e| e.versions.iter().map(|v| v.sha256.clone()))
|
||||
.collect(),
|
||||
// The first pull request has no entries to compare with.
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
let (entries, _) = registry::load(Path::new(head)).map_err(lines)?;
|
||||
let mut failed = 0;
|
||||
for e in &entries {
|
||||
for v in e.versions.iter().filter(|v| !old.contains(&v.sha256)) {
|
||||
println!("{} {} ({}):", e.id, v.version, v.file);
|
||||
let report = crate::check::check(
|
||||
&files.join(&v.sha256),
|
||||
e.kind.clone(),
|
||||
e.key.as_deref(),
|
||||
v.signature.as_deref(),
|
||||
);
|
||||
match report {
|
||||
Ok(r) => print!("{}", r.text()),
|
||||
Err(err) => {
|
||||
failed += 1;
|
||||
println!(" error: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("{failed} file(s) failed their check"))
|
||||
}
|
||||
}
|
||||
// The files CI must download for `build`: `<sha256> <id> <version> <file>`.
|
||||
Some("list") => {
|
||||
let [checkout] = pos[..] else {
|
||||
return Err(USAGE.into());
|
||||
};
|
||||
let (entries, _) = registry::load(Path::new(checkout)).map_err(lines)?;
|
||||
for e in &entries {
|
||||
for v in &e.versions {
|
||||
println!("{} {} {} {}", v.sha256, e.id, v.version, v.file);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.into()),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"template": "minecraft:amethyst_block",
|
||||
"properties": { "hardness": 2.0, "light": 7 }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"variants": {
|
||||
"": { "model": "{id}:block/glowing_amethyst" }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
id = "{id}"
|
||||
name = "{name}"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = []
|
||||
# A content mod: blocks and items from data/ and assets/, no code (API 0.2).
|
||||
api = "0.2"
|
||||
side = "both"
|
||||
# registries: "May add blocks and items to your worlds".
|
||||
capabilities = ["registries"]
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"model": { "type": "minecraft:model", "model": "{id}:block/glowing_amethyst" }
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"block.{id}.glowing_amethyst": "Glowing Amethyst"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"condition": { "type": "minecraft:survives_explosion" },
|
||||
"entries": [{ "type": "minecraft:item", "name": "{id}:glowing_amethyst" }],
|
||||
"rolls": 1
|
||||
}
|
||||
],
|
||||
"random_sequence": "{id}:blocks/glowing_amethyst"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"parent": "minecraft:block/cube_all",
|
||||
"textures": { "all": "minecraft:block/amethyst_block" }
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"values": ["{id}:glowing_amethyst"]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "building",
|
||||
"ingredients": ["minecraft:amethyst_block", "minecraft:glowstone_dust"],
|
||||
"result": { "id": "{id}:glowing_amethyst" }
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# `cargo horizon-mod test` runs every tests/*.toml against the built mod, with no game:
|
||||
# steps happen in order, and `expect` checks what the mod showed during the step.
|
||||
# Every key: docs/mods.md, "Testing a mod".
|
||||
|
||||
[[step]]
|
||||
expect.commands = ["hello"]
|
||||
|
||||
[[step]]
|
||||
command = "hello world"
|
||||
expect.chat = ["Hello world! (1 so far)"]
|
||||
|
||||
[[step]]
|
||||
command = "hello again"
|
||||
expect.chat = ["Hello again! (2 so far)"]
|
||||
@@ -4,15 +4,15 @@ use crate::json::{Result, error};
|
||||
use horizon_core::random::RandomSource;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Choice<T> {
|
||||
Single(T),
|
||||
enum Choice {
|
||||
Single(Variant),
|
||||
Weighted {
|
||||
variants: Vec<T>,
|
||||
variants: Vec<Variant>,
|
||||
ends: Vec<i32>,
|
||||
total: i32,
|
||||
},
|
||||
}
|
||||
impl Choice<Variant> {
|
||||
impl Choice {
|
||||
fn new(value: &VariantSet) -> Result<Self> {
|
||||
match value {
|
||||
VariantSet::Single(value) => Ok(Self::Single(value.clone())),
|
||||
@@ -41,23 +41,7 @@ impl Choice<Variant> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Choice<T> {
|
||||
fn map<U>(&self, f: &mut impl FnMut(&T) -> U) -> Choice<U> {
|
||||
match self {
|
||||
Self::Single(value) => Choice::Single(f(value)),
|
||||
Self::Weighted {
|
||||
variants,
|
||||
ends,
|
||||
total,
|
||||
} => Choice::Weighted {
|
||||
variants: variants.iter().map(&mut *f).collect(),
|
||||
ends: ends.clone(),
|
||||
total: *total,
|
||||
},
|
||||
}
|
||||
}
|
||||
fn select(&self, random: &mut impl RandomSource) -> &T {
|
||||
fn select(&self, random: &mut impl RandomSource) -> &Variant {
|
||||
match self {
|
||||
Self::Single(value) => value,
|
||||
Self::Weighted {
|
||||
@@ -76,11 +60,10 @@ impl<T> Choice<T> {
|
||||
}
|
||||
/// Compile once per definition and share across its bound block states. Prefix
|
||||
/// sums avoid rebuilding weight totals or scanning all entries during selection.
|
||||
/// `map` swaps each variant for what it stands for (its baked model).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompiledChoices<T = Variant> {
|
||||
variants: Vec<Choice<T>>,
|
||||
multipart: Vec<Choice<T>>,
|
||||
pub struct CompiledChoices {
|
||||
variants: Vec<Choice>,
|
||||
multipart: Vec<Choice>,
|
||||
}
|
||||
impl CompiledChoices {
|
||||
pub fn new(definition: &BlockStateDefinition) -> Result<Self> {
|
||||
@@ -99,45 +82,14 @@ impl CompiledChoices {
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<T> CompiledChoices<T> {
|
||||
/// The same choices, with `f` of each variant in its place.
|
||||
pub fn map<U>(&self, mut f: impl FnMut(&T) -> U) -> CompiledChoices<U> {
|
||||
CompiledChoices {
|
||||
variants: self.variants.iter().map(|c| c.map(&mut f)).collect(),
|
||||
multipart: self.multipart.iter().map(|c| c.map(&mut f)).collect(),
|
||||
}
|
||||
}
|
||||
/// 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`).
|
||||
pub fn particle_variant(&self, selected: &SelectedModels) -> Option<&T> {
|
||||
let choice = match selected {
|
||||
SelectedModels::Variant(index) => self.variants.get(*index)?,
|
||||
SelectedModels::Multipart(_) => self.multipart.first()?,
|
||||
};
|
||||
Some(match choice {
|
||||
Choice::Single(value) => value,
|
||||
Choice::Weighted { variants, .. } => variants.first()?,
|
||||
})
|
||||
}
|
||||
/// Append selected variants without allocating intermediate part lists.
|
||||
/// Append selected variants without allocating intermediate part lists. A
|
||||
/// multipart root consumes nextLong even for an empty selected subset, then
|
||||
/// resets the same random source to that seed before each selected part.
|
||||
pub fn collect<'a>(
|
||||
&'a self,
|
||||
selected: &SelectedModels,
|
||||
random: &mut impl RandomSource,
|
||||
output: &mut Vec<&'a T>,
|
||||
) -> Result<()> {
|
||||
self.for_each(selected, random, |v| output.push(v))
|
||||
}
|
||||
/// Each selected variant, in order. A multipart root consumes nextLong
|
||||
/// even for an empty selected subset, then resets the same random source
|
||||
/// to that seed before each selected part.
|
||||
pub fn for_each<'a>(
|
||||
&'a self,
|
||||
selected: &SelectedModels,
|
||||
random: &mut impl RandomSource,
|
||||
mut f: impl FnMut(&'a T),
|
||||
output: &mut Vec<&'a Variant>,
|
||||
) -> Result<()> {
|
||||
match selected {
|
||||
SelectedModels::Variant(index) => {
|
||||
@@ -145,7 +97,7 @@ impl<T> CompiledChoices<T> {
|
||||
.variants
|
||||
.get(*index)
|
||||
.ok_or_else(|| error("invalid variant selection"))?;
|
||||
f(choice.select(random));
|
||||
output.push(choice.select(random));
|
||||
}
|
||||
SelectedModels::Multipart(indices) => {
|
||||
if indices.iter().any(|index| *index >= self.multipart.len()) {
|
||||
@@ -154,7 +106,7 @@ impl<T> CompiledChoices<T> {
|
||||
let seed = random.next_long();
|
||||
for index in indices {
|
||||
random.set_seed(seed);
|
||||
f(self.multipart[*index].select(random));
|
||||
output.push(self.multipart[*index].select(random));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,22 +1031,19 @@ pub struct ItemModels {
|
||||
}
|
||||
|
||||
impl ItemModels {
|
||||
/// The stack's components: its item's defaults with the stack's patch on top. A stack
|
||||
/// with no patch (most stacks a screen draws) borrows the defaults instead of copying them.
|
||||
pub fn components(&self, stack: &StackView) -> std::borrow::Cow<'_, Components> {
|
||||
static NONE: Components = BTreeMap::new();
|
||||
let prototype = self.prototypes.get(&stack.item).unwrap_or(&NONE);
|
||||
if stack.patch.is_empty() {
|
||||
return std::borrow::Cow::Borrowed(prototype);
|
||||
}
|
||||
let mut out = prototype.clone();
|
||||
pub fn components(&self, stack: &StackView) -> Components {
|
||||
let mut out = self
|
||||
.prototypes
|
||||
.get(&stack.item)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
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)
|
||||
out
|
||||
}
|
||||
/// `ItemModelResolver.updateForTopItem`.
|
||||
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
|
||||
|
||||
@@ -8,5 +8,3 @@ description = "The benchmark report format (docs/benchmark-report.md), shared by
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# The hardware probes shared with telemetry (cores, clocks, drive, VRAM, power, form).
|
||||
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The machine a report was measured on. CPU, RAM and OS are detected here; the GPU and the display
|
||||
//! come from the program that rendered (the game knows its adapter, the launcher its window).
|
||||
|
||||
use horizon_telemetry::system::command_output as output;
|
||||
use std::process::Command;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -15,22 +16,8 @@ pub struct Hardware {
|
||||
pub cpu: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_threads: Option<usize>,
|
||||
/// Physical cores.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_cores: Option<u16>,
|
||||
/// Base and top boost clock, where the OS gives them.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_base_mhz: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_boost_mhz: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ram_mb: Option<u64>,
|
||||
/// The drive under the game folder: `nvme`, `ssd` or `hdd`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub storage: Option<String>,
|
||||
/// `laptop` or `desktop`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub form: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gpu: Option<Gpu>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -46,9 +33,6 @@ pub struct Gpu {
|
||||
pub backend: String,
|
||||
/// `integrated`, `discrete`, `virtual`, `cpu`, `other`.
|
||||
pub kind: String,
|
||||
/// Its own memory in MiB (rounded to 256), for a discrete GPU whose driver says.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub vram_mb: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -63,50 +47,17 @@ pub struct Display {
|
||||
|
||||
impl Hardware {
|
||||
pub fn detect() -> Hardware {
|
||||
let probes = probes();
|
||||
Hardware {
|
||||
os: std::env::consts::OS.to_owned(),
|
||||
os_version: os_version(),
|
||||
arch: std::env::consts::ARCH.to_owned(),
|
||||
cpu: cpu(),
|
||||
cpu_threads: std::thread::available_parallelism().ok().map(|n| n.get()),
|
||||
cpu_cores: probes.cores,
|
||||
cpu_base_mhz: probes.base,
|
||||
cpu_boost_mhz: probes.boost,
|
||||
ram_mb: ram_bytes().map(|b| b >> 20),
|
||||
storage: None,
|
||||
form: probes.form,
|
||||
gpu: None,
|
||||
display: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hardware::detect`] with the kind of drive `game_dir` is on.
|
||||
pub fn detect_in(game_dir: &std::path::Path) -> Hardware {
|
||||
Hardware {
|
||||
storage: horizon_telemetry::system::storage(game_dir).map(str::to_owned),
|
||||
..Hardware::detect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the telemetry probes read (`horizon_telemetry::system`).
|
||||
struct Probes {
|
||||
cores: Option<u16>,
|
||||
base: Option<u32>,
|
||||
boost: Option<u32>,
|
||||
form: Option<String>,
|
||||
}
|
||||
|
||||
fn probes() -> Probes {
|
||||
let s = horizon_telemetry::payload::System::detect();
|
||||
let some = |n: u32| (n > 0).then_some(n);
|
||||
Probes {
|
||||
cores: (s.cores > 0).then_some(s.cores),
|
||||
base: some(s.cpu_base_mhz),
|
||||
boost: some(s.cpu_boost_mhz),
|
||||
form: (!s.form.is_empty()).then_some(s.form),
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-minute load average: above about the number of threads the machine was busy.
|
||||
@@ -131,7 +82,25 @@ pub fn load_average() -> Option<f64> {
|
||||
|
||||
/// `ac` or `battery`; `None` where it can't be read (and on machines without a battery on Linux).
|
||||
pub fn power_source() -> Option<&'static str> {
|
||||
horizon_telemetry::system::power()
|
||||
if cfg!(target_os = "macos") {
|
||||
let batt = output("pmset", &["-g", "batt"])?;
|
||||
return Some(if batt.contains("'Battery Power'") {
|
||||
"battery"
|
||||
} else {
|
||||
"ac"
|
||||
});
|
||||
}
|
||||
let mut on_battery = None;
|
||||
for entry in std::fs::read_dir("/sys/class/power_supply").ok()?.flatten() {
|
||||
let read =
|
||||
|name: &str| std::fs::read_to_string(entry.path().join(name)).unwrap_or_default();
|
||||
match read("type").trim() {
|
||||
"Mains" if read("online").trim() == "1" => return Some("ac"),
|
||||
"Battery" => on_battery = Some("battery"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
on_battery
|
||||
}
|
||||
|
||||
/// Resident memory of a process, in bytes (macOS, Linux).
|
||||
@@ -172,6 +141,12 @@ pub fn cpu_seconds(pid: u32) -> Option<f64> {
|
||||
Some(ticks / 100.0)
|
||||
}
|
||||
|
||||
fn output(program: &str, args: &[&str]) -> Option<String> {
|
||||
let out = Command::new(program).args(args).output().ok()?;
|
||||
let text = String::from_utf8(out.stdout).ok()?.trim().to_owned();
|
||||
(out.status.success() && !text.is_empty()).then_some(text)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn os_version() -> Option<String> {
|
||||
output("sw_vers", &["-productVersion"])
|
||||
|
||||
@@ -245,19 +245,13 @@ pub struct Report {
|
||||
impl Report {
|
||||
/// An empty report for this machine, created now.
|
||||
pub fn new(kind: Kind, app: App) -> Report {
|
||||
Self::with_hardware(kind, app, Hardware::detect())
|
||||
}
|
||||
|
||||
/// Starts a report using supplied hardware, without running system probes.
|
||||
/// Interactive apps can collect the remaining machine details after their first frame.
|
||||
pub fn with_hardware(kind: Kind, app: App, hardware: Hardware) -> Report {
|
||||
Report {
|
||||
format: FORMAT.to_owned(),
|
||||
version: VERSION,
|
||||
kind,
|
||||
created: utc_now(),
|
||||
app,
|
||||
hardware,
|
||||
hardware: Hardware::detect(),
|
||||
conditions: Conditions::default(),
|
||||
metrics: BTreeMap::new(),
|
||||
frames: Vec::new(),
|
||||
|
||||
+34
-142
@@ -9,11 +9,6 @@
|
||||
//! horizon-bots --server 127.0.0.1:25565 --bots 25 --behaviour mixed --seconds 360 --report bots.json
|
||||
//!
|
||||
//! It prints one JSON line a second (bots online, chunks received) and writes a summary with `--report`.
|
||||
//! A bot moves only while it holds the chunk it stands in: vanilla's client stops ticking its player in a chunk
|
||||
//! it hasn't received, so a player can't outrun the server's chunk sending, on any server.
|
||||
//! Each bot also keeps the chunks it holds, as a client's chunk cache does, so the report tells chunks that
|
||||
//! entered the view apart from chunks sent again while the bot still held them (`resent`), and counts the
|
||||
//! times the server pulled a bot back (`pulled_back`: a teleport after the first, as for "moved too quickly").
|
||||
|
||||
use horizon_protocol::{
|
||||
PROTOCOL_VERSION,
|
||||
@@ -30,7 +25,6 @@ use horizon_protocol::{
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
@@ -49,9 +43,8 @@ horizon-bots: load-test bots for a Minecraft Java Edition 26.3 server (offline m
|
||||
--seconds N how long to run after the first bot joins; default 60
|
||||
--view-distance N what the bots ask for; default 10
|
||||
--seed N default 12345
|
||||
--explore-speed B blocks a second an explore bot flies; default 22 (as fast as chunks come)
|
||||
--name PREFIX bot names: PREFIX0, PREFIX1...; default Bot
|
||||
--report FILE a JSON summary at the end (- prints it after the per-second lines)
|
||||
--report FILE a JSON summary at the end
|
||||
";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -80,7 +73,6 @@ struct Options {
|
||||
seconds: u64,
|
||||
view_distance: i8,
|
||||
seed: u64,
|
||||
explore_speed: f64,
|
||||
name: String,
|
||||
report: Option<String>,
|
||||
}
|
||||
@@ -95,7 +87,6 @@ fn options() -> Result<Options, String> {
|
||||
seconds: 60,
|
||||
view_distance: 10,
|
||||
seed: 12345,
|
||||
explore_speed: 22.0,
|
||||
name: "Bot".into(),
|
||||
report: None,
|
||||
};
|
||||
@@ -119,9 +110,6 @@ fn options() -> Result<Options, String> {
|
||||
"--seconds" => o.seconds = value.parse().map_err(|_| number("seconds"))?,
|
||||
"--view-distance" => o.view_distance = value.parse().map_err(|_| number("2 to 32"))?,
|
||||
"--seed" => o.seed = value.parse().map_err(|_| number("a number"))?,
|
||||
"--explore-speed" => {
|
||||
o.explore_speed = value.parse().map_err(|_| number("blocks a second"))?;
|
||||
}
|
||||
"--name" => o.name = value,
|
||||
"--report" => o.report = Some(value),
|
||||
_ => return Err(format!("unknown option {arg}; --help lists them")),
|
||||
@@ -141,17 +129,6 @@ fn options() -> Result<Options, String> {
|
||||
struct Bot {
|
||||
online: AtomicBool,
|
||||
chunks: AtomicU64,
|
||||
/// Chunks received while the bot already held them (no ForgetLevelChunk in between).
|
||||
resent: AtomicU64,
|
||||
/// Different chunks received over the run.
|
||||
unique: AtomicU64,
|
||||
forgotten: AtomicU64,
|
||||
/// Teleports after the spawn one: the server refused a move.
|
||||
pulled_back: AtomicU64,
|
||||
/// Ticks the bot couldn't move because it didn't hold its chunk yet.
|
||||
waited: AtomicU64,
|
||||
/// The chunks the bot holds (ClientChunkCache).
|
||||
held: Mutex<HashSet<(i32, i32)>>,
|
||||
/// Where the server last put the bot (a teleport), to start from.
|
||||
teleport: Mutex<Option<[f64; 3]>>,
|
||||
error: Mutex<Option<String>>,
|
||||
@@ -173,11 +150,6 @@ impl Random {
|
||||
}
|
||||
}
|
||||
|
||||
/// One bot thread that panics poisons the locks it held; the others and the report go on.
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn raw(id: i32, write: impl FnOnce(&mut Writer)) -> Vec<u8> {
|
||||
let mut w = Writer::new();
|
||||
w.varint(id);
|
||||
@@ -296,36 +268,6 @@ fn join(o: &Options, name: &str) -> Result<Connection, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// ChunkBatchSizeCalculator: the chunks per tick a vanilla client asks for, from how long its batches take
|
||||
/// to arrive.
|
||||
struct BatchSize {
|
||||
nanos_per_chunk: f64,
|
||||
old_samples_weight: u32,
|
||||
started: Instant,
|
||||
}
|
||||
|
||||
impl BatchSize {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
nanos_per_chunk: 2_000_000.0,
|
||||
old_samples_weight: 1,
|
||||
started: Instant::now(),
|
||||
}
|
||||
}
|
||||
fn finished(&mut self, size: i32) {
|
||||
if size > 0 {
|
||||
let per_chunk = self.started.elapsed().as_nanos() as f64 / f64::from(size);
|
||||
let clamped = per_chunk.clamp(self.nanos_per_chunk / 3.0, self.nanos_per_chunk * 3.0);
|
||||
let weight = f64::from(self.old_samples_weight);
|
||||
self.nanos_per_chunk = (self.nanos_per_chunk * weight + clamped) / (weight + 1.0);
|
||||
self.old_samples_weight = (self.old_samples_weight + 1).min(49);
|
||||
}
|
||||
}
|
||||
fn desired_chunks_per_tick(&self) -> f32 {
|
||||
(7_000_000.0 / self.nanos_per_chunk) as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the play state: answers what a client must answer, counts chunks, records teleports.
|
||||
fn read(
|
||||
mut reader: horizon_protocol::connection::ConnReader,
|
||||
@@ -334,16 +276,13 @@ fn read(
|
||||
) {
|
||||
use ids::play::clientbound as cb;
|
||||
use ids::play::serverbound as sb;
|
||||
let send = |body: Vec<u8>| lock(&writer).send(&body);
|
||||
let mut seen = HashSet::new();
|
||||
let mut batch = BatchSize::new();
|
||||
let mut teleports = 0u64;
|
||||
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body);
|
||||
loop {
|
||||
let (id, body) = match reader.recv() {
|
||||
Ok(packet) => packet,
|
||||
Err(e) => {
|
||||
if bot.online.swap(false, Ordering::SeqCst) {
|
||||
*lock(&bot.error) = Some(e.to_string());
|
||||
*bot.error.lock().unwrap() = Some(e.to_string());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -360,41 +299,17 @@ fn read(
|
||||
},
|
||||
cb::LEVEL_CHUNK_WITH_LIGHT => {
|
||||
bot.chunks.fetch_add(1, Ordering::Relaxed);
|
||||
if let (Ok(x), Ok(z)) = (r.i32(), r.i32()) {
|
||||
if !lock(&bot.held).insert((x, z)) {
|
||||
bot.resent.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if seen.insert((x, z)) {
|
||||
bot.unique.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
cb::FORGET_LEVEL_CHUNK => {
|
||||
if let Ok(play::Clientbound::ForgetLevelChunk(p)) =
|
||||
play::Clientbound::decode(id, &body)
|
||||
&& lock(&bot.held).remove(&(p.pos[0], p.pos[1]))
|
||||
{
|
||||
bot.forgotten.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
cb::CHUNK_BATCH_START => {
|
||||
batch.started = Instant::now();
|
||||
Ok(())
|
||||
}
|
||||
cb::CHUNK_BATCH_FINISHED => {
|
||||
batch.finished(r.varint().unwrap_or(0));
|
||||
send(
|
||||
play::Serverbound::ChunkBatchReceived(play::ChunkBatchReceived {
|
||||
chunks_per_tick: batch.desired_chunks_per_tick(),
|
||||
})
|
||||
.encode(),
|
||||
)
|
||||
}
|
||||
cb::CHUNK_BATCH_FINISHED => send(
|
||||
play::Serverbound::ChunkBatchReceived(play::ChunkBatchReceived {
|
||||
chunks_per_tick: 25.0,
|
||||
})
|
||||
.encode(),
|
||||
),
|
||||
cb::PLAYER_POSITION => match play::Clientbound::decode(id, &body) {
|
||||
Ok(play::Clientbound::PlayerPosition(p)) => {
|
||||
let mut at = lock(&bot.teleport);
|
||||
let mut at = bot.teleport.lock().unwrap();
|
||||
let previous = at.unwrap_or([0.0; 3]);
|
||||
// Relative flags: X 1, Y 2, Z 4.
|
||||
let pos: [f64; 3] = std::array::from_fn(|i| {
|
||||
@@ -405,10 +320,6 @@ fn read(
|
||||
}
|
||||
});
|
||||
*at = Some(pos);
|
||||
teleports += 1;
|
||||
if teleports > 1 {
|
||||
bot.pulled_back.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
send(
|
||||
play::Serverbound::AcceptTeleportation(play::AcceptTeleportation {
|
||||
id: p.teleport,
|
||||
@@ -425,7 +336,7 @@ fn read(
|
||||
},
|
||||
cb::DISCONNECT | cb::START_CONFIGURATION => {
|
||||
bot.online.store(false, Ordering::SeqCst);
|
||||
*lock(&bot.error) = Some(if id == cb::DISCONNECT {
|
||||
*bot.error.lock().unwrap() = Some(if id == cb::DISCONNECT {
|
||||
format!(
|
||||
"disconnected: {}",
|
||||
String::from_utf8_lossy(&body)
|
||||
@@ -460,7 +371,7 @@ fn run_bot(
|
||||
let connection = match join(&o, &name) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
*lock(&bot.error) = Some(e);
|
||||
*bot.error.lock().unwrap() = Some(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -474,12 +385,12 @@ fn run_bot(
|
||||
use ids::play::serverbound as sb;
|
||||
// Wait for the spawn position.
|
||||
let spawn = loop {
|
||||
if let Some(at) = *lock(&bot.teleport) {
|
||||
if let Some(at) = *bot.teleport.lock().unwrap() {
|
||||
break at;
|
||||
}
|
||||
if !bot.online.load(Ordering::SeqCst) || asked.elapsed() > Duration::from_secs(60) {
|
||||
if lock(&bot.error).is_none() {
|
||||
*lock(&bot.error) = Some("no spawn position within 60 s".into());
|
||||
if bot.error.lock().unwrap().is_none() {
|
||||
*bot.error.lock().unwrap() = Some("no spawn position within 60 s".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -487,7 +398,7 @@ fn run_bot(
|
||||
};
|
||||
bot.joined_after_ms
|
||||
.store(asked.elapsed().as_millis() as u64, Ordering::SeqCst);
|
||||
let send = |body: Vec<u8>| lock(&writer).send(&body).is_ok();
|
||||
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body).is_ok();
|
||||
send(raw(sb::PLAYER_LOADED, |_| {}));
|
||||
let mut random = Random(o.seed ^ (index as u64).wrapping_mul(0x2545_F491_4F6C_DD1D));
|
||||
let mut pos = spawn;
|
||||
@@ -496,17 +407,16 @@ fn run_bot(
|
||||
let mut target = pos;
|
||||
let mut next = Instant::now();
|
||||
loop {
|
||||
if lock(&until).is_some_and(|u| Instant::now() >= u) || !bot.online.load(Ordering::SeqCst) {
|
||||
if until.lock().unwrap().is_some_and(|u| Instant::now() >= u)
|
||||
|| !bot.online.load(Ordering::SeqCst)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// A teleport from the server wins over where the bot thought it was.
|
||||
if let Some(at) = lock(&bot.teleport).take() {
|
||||
if let Some(at) = bot.teleport.lock().unwrap().take() {
|
||||
pos = at;
|
||||
}
|
||||
let here = ((pos[0].floor() as i32) >> 4, (pos[2].floor() as i32) >> 4);
|
||||
if !lock(&bot.held).contains(&here) {
|
||||
bot.waited.fetch_add(1, Ordering::Relaxed);
|
||||
} else if pos[1] < cruise {
|
||||
if pos[1] < cruise {
|
||||
pos[1] = (pos[1] + 0.5).min(cruise);
|
||||
} else {
|
||||
match behaviour {
|
||||
@@ -530,11 +440,9 @@ fn run_bot(
|
||||
}
|
||||
}
|
||||
Behaviour::Explore => {
|
||||
// Flying in creative, by default about 22 blocks a second, which keeps new
|
||||
// chunks coming as fast as the server sends them.
|
||||
let step = o.explore_speed / 20.0;
|
||||
pos[0] += heading.cos() * step;
|
||||
pos[2] += heading.sin() * step;
|
||||
// Flying fast in creative: about 22 blocks a second, which keeps new chunks coming.
|
||||
pos[0] += heading.cos() * 1.1;
|
||||
pos[2] += heading.sin() * 1.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,7 +469,7 @@ fn run_bot(
|
||||
}
|
||||
// Offline first, so the reader doesn't count the closed connection as a failure.
|
||||
bot.online.store(false, Ordering::SeqCst);
|
||||
lock(&writer).shutdown();
|
||||
writer.lock().unwrap().shutdown();
|
||||
}
|
||||
|
||||
fn behaviour_of(mode: &str, index: usize) -> Behaviour {
|
||||
@@ -612,13 +520,13 @@ fn main() {
|
||||
.filter(|b| b.online.load(Ordering::SeqCst))
|
||||
.count();
|
||||
let chunks: u64 = bots.iter().map(|b| b.chunks.load(Ordering::Relaxed)).sum();
|
||||
let total = |f: fn(&Bot) -> &AtomicU64| -> u64 {
|
||||
bots.iter().map(|b| f(b).load(Ordering::Relaxed)).sum()
|
||||
};
|
||||
let failed = bots.iter().filter(|b| lock(&b.error).is_some()).count();
|
||||
let failed = bots
|
||||
.iter()
|
||||
.filter(|b| b.error.lock().unwrap().is_some())
|
||||
.count();
|
||||
if first.is_none() && online > 0 {
|
||||
first = Some(Instant::now());
|
||||
*lock(&until) = Some(Instant::now() + Duration::from_secs(o.seconds));
|
||||
*until.lock().unwrap() = Some(Instant::now() + Duration::from_secs(o.seconds));
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
@@ -628,18 +536,13 @@ fn main() {
|
||||
"failed": failed,
|
||||
"chunks": chunks,
|
||||
"chunks_per_second": chunks - previous_chunks,
|
||||
"unique": total(|b| &b.unique),
|
||||
"forgotten": total(|b| &b.forgotten),
|
||||
"resent": total(|b| &b.resent),
|
||||
"pulled_back": total(|b| &b.pulled_back),
|
||||
"waited_ticks": total(|b| &b.waited),
|
||||
})
|
||||
);
|
||||
previous_chunks = chunks;
|
||||
let over = lock(&until).is_some_and(|u| Instant::now() >= u);
|
||||
let over = until.lock().unwrap().is_some_and(|u| Instant::now() >= u);
|
||||
let all_failed = failed == o.bots;
|
||||
if over || all_failed || (first.is_none() && start.elapsed() > Duration::from_secs(120)) {
|
||||
*lock(&until) = Some(Instant::now());
|
||||
*until.lock().unwrap() = Some(Instant::now());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -651,30 +554,19 @@ fn main() {
|
||||
"bots": o.bots,
|
||||
"behaviour": o.behaviour,
|
||||
"seed": o.seed,
|
||||
"explore_speed": o.explore_speed,
|
||||
"view_distance": o.view_distance,
|
||||
"seconds": o.seconds,
|
||||
"joined": bots.iter().filter(|b| b.joined_after_ms.load(Ordering::SeqCst) > 0).count(),
|
||||
"chunks": bots.iter().map(|b| b.chunks.load(Ordering::Relaxed)).sum::<u64>(),
|
||||
"resent": bots.iter().map(|b| b.resent.load(Ordering::Relaxed)).sum::<u64>(),
|
||||
"pulled_back": bots.iter().map(|b| b.pulled_back.load(Ordering::Relaxed)).sum::<u64>(),
|
||||
"waited_ticks": bots.iter().map(|b| b.waited.load(Ordering::Relaxed)).sum::<u64>(),
|
||||
"per_bot": bots.iter().enumerate().map(|(i, b)| json!({
|
||||
"name": format!("{}{i}", o.name),
|
||||
"behaviour": behaviour_of(&o.behaviour, i).name(),
|
||||
"joined_after_ms": b.joined_after_ms.load(Ordering::SeqCst),
|
||||
"chunks": b.chunks.load(Ordering::Relaxed),
|
||||
"unique_chunks": b.unique.load(Ordering::Relaxed),
|
||||
"resent": b.resent.load(Ordering::Relaxed),
|
||||
"forgotten": b.forgotten.load(Ordering::Relaxed),
|
||||
"pulled_back": b.pulled_back.load(Ordering::Relaxed),
|
||||
"waited_ticks": b.waited.load(Ordering::Relaxed),
|
||||
"error": *lock(&b.error),
|
||||
"error": *b.error.lock().unwrap(),
|
||||
})).collect::<Vec<_>>(),
|
||||
});
|
||||
if o.report.as_deref() == Some("-") {
|
||||
println!("{summary:#}");
|
||||
} else if let Some(path) = &o.report {
|
||||
if let Some(path) = &o.report {
|
||||
if let Err(e) = std::fs::write(path, format!("{summary:#}\n")) {
|
||||
eprintln!("horizon-bots: {path}: {e}");
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "horizon-client-core"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.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]
|
||||
horizon-world = { path = "../horizon-world" }
|
||||
serde_json = { version = "1.0.151", features = ["raw_value"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,18 +0,0 @@
|
||||
//! Resource identifiers (`Identifier`).
|
||||
|
||||
/// Identifier.parse: `ns:path` or `path`, validated.
|
||||
pub fn parse_identifier(raw: &str) -> Option<String> {
|
||||
let (ns, path) = match raw.split_once(':') {
|
||||
Some(("", path)) => ("minecraft", path),
|
||||
Some((ns, path)) => (ns, path),
|
||||
None => ("minecraft", raw),
|
||||
};
|
||||
let ns_ok = ns != ".."
|
||||
&& ns
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-'));
|
||||
let path_ok = path.chars().all(|c| {
|
||||
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-' | '/')
|
||||
});
|
||||
(ns_ok && path_ok).then(|| format!("{ns}:{path}"))
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//! Key display names (`InputConstants.Key.getDisplayName`), shared by key
|
||||
//! mappings and `keybind` text components.
|
||||
use crate::text::Text;
|
||||
|
||||
/// InputConstants.Key.getDisplayName: one-character keys as the upper-case
|
||||
/// character, others by translation.
|
||||
pub fn display_name(key: &str) -> Text {
|
||||
if let Some(rest) = key.strip_prefix("key.keyboard.")
|
||||
&& rest.chars().count() == 1
|
||||
{
|
||||
let c = rest.to_uppercase();
|
||||
return Text::literal(c);
|
||||
}
|
||||
let symbol = match key {
|
||||
"key.keyboard.minus" => Some("-"),
|
||||
"key.keyboard.equal" => Some("="),
|
||||
"key.keyboard.left.bracket" => Some("["),
|
||||
"key.keyboard.right.bracket" => Some("]"),
|
||||
"key.keyboard.backslash" => Some("\\"),
|
||||
"key.keyboard.semicolon" => Some(";"),
|
||||
"key.keyboard.apostrophe" => Some("'"),
|
||||
"key.keyboard.grave.accent" => Some("`"),
|
||||
"key.keyboard.comma" => Some(","),
|
||||
"key.keyboard.period" => Some("."),
|
||||
"key.keyboard.slash" => Some("/"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(s) = symbol {
|
||||
return Text::literal(s);
|
||||
}
|
||||
if let Some(n) = key.strip_prefix("key.mouse.")
|
||||
&& n.parse::<u32>().is_ok()
|
||||
&& !crate::lang::has(key)
|
||||
{
|
||||
return Text::translate("key.mouse", &[Text::literal(n)]);
|
||||
}
|
||||
Text::translate(key, &[])
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//! The client's lowest layer, below commands and gameplay: chat text and
|
||||
//! translations, SNBT and NBT paths, text components, the scoreboard, boss
|
||||
//! bars, game rules, block entity NBT tables, and the registry, particle and
|
||||
//! sound source tables commands name, and the `pack.mcmeta` filter regex.
|
||||
//! horizon-client re-exports each module under its old path
|
||||
//! (`horizon_client::text`, ...).
|
||||
#![forbid(unsafe_code)]
|
||||
pub mod block_entity_nbt;
|
||||
pub mod bossbar;
|
||||
pub mod component;
|
||||
pub mod fastutil_order;
|
||||
pub mod gamerules;
|
||||
pub mod identifier;
|
||||
pub mod key_display;
|
||||
pub mod lang;
|
||||
pub mod nbt_path;
|
||||
pub mod nbt_value;
|
||||
pub mod particle_type;
|
||||
pub mod pattern;
|
||||
pub mod potions;
|
||||
pub mod registries;
|
||||
pub mod scoreboard;
|
||||
pub mod snbt;
|
||||
pub mod sound_source;
|
||||
pub mod text;
|
||||
pub mod uuid;
|
||||
pub mod who;
|
||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
@@ -1,24 +0,0 @@
|
||||
//! Reading and writing single NBT values the way vanilla's codecs accept them.
|
||||
use horizon_world::nbt::Tag;
|
||||
|
||||
pub fn string(s: &str) -> Tag {
|
||||
Tag::String(s.into())
|
||||
}
|
||||
pub fn text(tag: Option<&Tag>) -> Option<String> {
|
||||
match tag {
|
||||
Some(Tag::String(s)) => Some(s.to_string_lossy()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Any numeric tag as i64 (vanilla's `getIntOr` etc. accept every number type).
|
||||
pub fn number(tag: Option<&Tag>) -> Option<i64> {
|
||||
Some(match tag? {
|
||||
Tag::Byte(v) => i64::from(*v),
|
||||
Tag::Short(v) => i64::from(*v),
|
||||
Tag::Int(v) => i64::from(*v),
|
||||
Tag::Long(v) => *v,
|
||||
Tag::Float(b) => f32::from_bits(*b) as i64,
|
||||
Tag::Double(b) => f64::from_bits(*b) as i64,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
//! Particle types (`ParticleTypes`): the vanilla particle types with a client
|
||||
//! implementation, by registry name, and their per-type flags.
|
||||
|
||||
macro_rules! particle_types {
|
||||
($($variant:ident = $name:literal),* $(,)?) => {
|
||||
/// Vanilla particle types with a client implementation here.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ParticleType { $($variant),* }
|
||||
impl ParticleType {
|
||||
pub const ALL: &'static [ParticleType] = &[$(ParticleType::$variant),*];
|
||||
/// Registry path (`minecraft:<name>`), also the sprite-set file name.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self { $(ParticleType::$variant => $name),* }
|
||||
}
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
let name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
match name { $($name => Some(ParticleType::$variant),)* _ => None }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
particle_types! {
|
||||
Block = "block",
|
||||
BlockCrumble = "block_crumble",
|
||||
Trail = "trail",
|
||||
DustPillar = "dust_pillar",
|
||||
FallingDust = "falling_dust",
|
||||
Smoke = "smoke",
|
||||
LargeSmoke = "large_smoke",
|
||||
WhiteSmoke = "white_smoke",
|
||||
Flame = "flame",
|
||||
SoulFireFlame = "soul_fire_flame",
|
||||
CopperFireFlame = "copper_fire_flame",
|
||||
SmallFlame = "small_flame",
|
||||
Lava = "lava",
|
||||
DrippingWater = "dripping_water",
|
||||
FallingWater = "falling_water",
|
||||
DrippingLava = "dripping_lava",
|
||||
FallingLava = "falling_lava",
|
||||
LandingLava = "landing_lava",
|
||||
DrippingHoney = "dripping_honey",
|
||||
FallingHoney = "falling_honey",
|
||||
LandingHoney = "landing_honey",
|
||||
FallingNectar = "falling_nectar",
|
||||
FallingSporeBlossom = "falling_spore_blossom",
|
||||
SporeBlossomAir = "spore_blossom_air",
|
||||
DrippingObsidianTear = "dripping_obsidian_tear",
|
||||
FallingObsidianTear = "falling_obsidian_tear",
|
||||
LandingObsidianTear = "landing_obsidian_tear",
|
||||
DrippingDripstoneWater = "dripping_dripstone_water",
|
||||
FallingDripstoneWater = "falling_dripstone_water",
|
||||
DrippingDripstoneLava = "dripping_dripstone_lava",
|
||||
FallingDripstoneLava = "falling_dripstone_lava",
|
||||
Splash = "splash",
|
||||
Rain = "rain",
|
||||
Bubble = "bubble",
|
||||
SulfurBubbles = "sulfur_bubbles",
|
||||
BubblePop = "bubble_pop",
|
||||
BubbleColumnUp = "bubble_column_up",
|
||||
CurrentDown = "current_down",
|
||||
Underwater = "underwater",
|
||||
Portal = "portal",
|
||||
ReversePortal = "reverse_portal",
|
||||
Enchant = "enchant",
|
||||
Nautilus = "nautilus",
|
||||
VaultConnection = "vault_connection",
|
||||
Crit = "crit",
|
||||
EnchantedHit = "enchanted_hit",
|
||||
DamageIndicator = "damage_indicator",
|
||||
Heart = "heart",
|
||||
AngryVillager = "angry_villager",
|
||||
HappyVillager = "happy_villager",
|
||||
Composter = "composter",
|
||||
Mycelium = "mycelium",
|
||||
Dolphin = "dolphin",
|
||||
EggCrack = "egg_crack",
|
||||
Poof = "poof",
|
||||
Explosion = "explosion",
|
||||
ExplosionEmitter = "explosion_emitter",
|
||||
CampfireCosySmoke = "campfire_cosy_smoke",
|
||||
CampfireSignalSmoke = "campfire_signal_smoke",
|
||||
Note = "note",
|
||||
Dust = "dust",
|
||||
TotemOfUndying = "totem_of_undying",
|
||||
Firework = "firework",
|
||||
EndRod = "end_rod",
|
||||
Ash = "ash",
|
||||
WhiteAsh = "white_ash",
|
||||
CrimsonSpore = "crimson_spore",
|
||||
WarpedSpore = "warped_spore",
|
||||
CherryLeaves = "cherry_leaves",
|
||||
PaleOakLeaves = "pale_oak_leaves",
|
||||
TintedLeaves = "tinted_leaves",
|
||||
Effect = "effect",
|
||||
InstantEffect = "instant_effect",
|
||||
EntityEffect = "entity_effect",
|
||||
Witch = "witch",
|
||||
Infested = "infested",
|
||||
Cloud = "cloud",
|
||||
Sneeze = "sneeze",
|
||||
Snowflake = "snowflake",
|
||||
Soul = "soul",
|
||||
SculkSoul = "sculk_soul",
|
||||
DragonBreath = "dragon_breath",
|
||||
Gust = "gust",
|
||||
SmallGust = "small_gust",
|
||||
GustEmitterLarge = "gust_emitter_large",
|
||||
GustEmitterSmall = "gust_emitter_small",
|
||||
DustColorTransition = "dust_color_transition",
|
||||
SculkCharge = "sculk_charge",
|
||||
SculkChargePop = "sculk_charge_pop",
|
||||
Shriek = "shriek",
|
||||
SonicBoom = "sonic_boom",
|
||||
Vibration = "vibration",
|
||||
SweepAttack = "sweep_attack",
|
||||
Glow = "glow",
|
||||
WaxOn = "wax_on",
|
||||
WaxOff = "wax_off",
|
||||
ElectricSpark = "electric_spark",
|
||||
Scrape = "scrape",
|
||||
SquidInk = "squid_ink",
|
||||
GlowSquidInk = "glow_squid_ink",
|
||||
Item = "item",
|
||||
ItemSlime = "item_slime",
|
||||
ItemSnowball = "item_snowball",
|
||||
ItemCobweb = "item_cobweb",
|
||||
SulfurCubeGoo = "sulfur_cube_goo",
|
||||
Flash = "flash",
|
||||
Firefly = "firefly",
|
||||
TrialSpawnerDetection = "trial_spawner_detection",
|
||||
TrialSpawnerDetectionOminous = "trial_spawner_detection_ominous",
|
||||
TrialOmen = "trial_omen",
|
||||
RaidOmen = "raid_omen",
|
||||
OminousSpawning = "ominous_spawning",
|
||||
Spit = "spit",
|
||||
Fishing = "fishing",
|
||||
DustPlume = "dust_plume",
|
||||
RedPoplarLeaves = "red_poplar_leaves",
|
||||
OrangePoplarLeaves = "orange_poplar_leaves",
|
||||
YellowPoplarLeaves = "yellow_poplar_leaves",
|
||||
PauseMobGrowth = "pause_mob_growth",
|
||||
ResetMobGrowth = "reset_mob_growth",
|
||||
BlockMarker = "block_marker",
|
||||
}
|
||||
impl ParticleType {
|
||||
/// `ParticleType.getOverrideLimiter` (`ParticleTypes.register(name, true)`):
|
||||
/// spawned regardless of distance.
|
||||
pub fn override_limiter(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::BlockMarker
|
||||
| Self::DamageIndicator
|
||||
| Self::ExplosionEmitter
|
||||
| Self::Explosion
|
||||
| Self::Gust
|
||||
| Self::GustEmitterLarge
|
||||
| Self::GustEmitterSmall
|
||||
| Self::SonicBoom
|
||||
| Self::SculkCharge
|
||||
| Self::SculkChargePop
|
||||
| Self::Vibration
|
||||
| Self::Poof
|
||||
| Self::Spit
|
||||
| Self::SquidInk
|
||||
| Self::SweepAttack
|
||||
| Self::CampfireCosySmoke
|
||||
| Self::CampfireSignalSmoke
|
||||
| Self::GlowSquidInk
|
||||
| Self::Glow
|
||||
| Self::WaxOn
|
||||
| Self::WaxOff
|
||||
| Self::ElectricSpark
|
||||
| Self::Scrape
|
||||
| Self::TrialSpawnerDetection
|
||||
| Self::TrialSpawnerDetectionOminous
|
||||
| Self::VaultConnection
|
||||
| Self::OminousSpawning
|
||||
)
|
||||
}
|
||||
/// `SingleQuadParticle.Layer.translucent`: types whose vanilla particle class
|
||||
/// hardcodes the translucent layer, drawn after translucent terrain.
|
||||
/// `TerrainParticle`, `BreakingItemParticle` and `BlockMarker` resolve their
|
||||
/// layer from the sprite instead (`Layer.bySprite`); see the `translucent`
|
||||
/// sprite set on the engine.
|
||||
pub fn translucent_layer(self) -> bool {
|
||||
match self {
|
||||
// CampfireSmokeParticle.
|
||||
Self::CampfireCosySmoke | Self::CampfireSignalSmoke
|
||||
// PlayerCloudParticle.
|
||||
| Self::Cloud | Self::Sneeze
|
||||
// SpellParticle.
|
||||
| Self::Effect | Self::InstantEffect | Self::EntityEffect
|
||||
| Self::Witch | Self::Infested | Self::TrialOmen | Self::RaidOmen
|
||||
// EmissiveRisingParticle.
|
||||
| Self::Soul | Self::SculkSoul
|
||||
// SculkChargeParticle, SculkChargePopParticle, ShriekParticle,
|
||||
// VibrationSignalParticle, FireflyParticle, OverlayParticle.
|
||||
| Self::SculkCharge | Self::SculkChargePop | Self::Shriek
|
||||
| Self::Vibration | Self::Firefly | Self::Flash
|
||||
// SimpleAnimatedParticle and its subclasses: EndRodParticle,
|
||||
// SparkParticle (firework), SquidInkParticle, TotemParticle.
|
||||
| Self::EndRod | Self::Firework | Self::SquidInk | Self::GlowSquidInk
|
||||
| Self::TotemOfUndying
|
||||
// FlyTowardsPositionParticle: its LifetimeAlpha(0, 0.6, 0.25, 1) is not opaque.
|
||||
| Self::VaultConnection => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
//! The `MobEffects` and `Potions` registries (code in vanilla, ported as
|
||||
//! tables): each effect's colour and category, each potion's effects, and the
|
||||
//! potion colour mix. horizon-client re-exports them from `brewing`.
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
Beneficial,
|
||||
Harmful,
|
||||
Neutral,
|
||||
}
|
||||
/// A `MobEffect`: id (without namespace), colour, category, instantaneous.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct EffectDef {
|
||||
pub id: &'static str,
|
||||
pub color: u32,
|
||||
pub category: Category,
|
||||
pub instant: bool,
|
||||
}
|
||||
const fn fx(id: &'static str, color: u32, category: Category) -> EffectDef {
|
||||
EffectDef {
|
||||
id,
|
||||
color,
|
||||
category,
|
||||
instant: false,
|
||||
}
|
||||
}
|
||||
use Category::{Beneficial as B, Harmful as H, Neutral as N};
|
||||
/// `MobEffects` in registration order.
|
||||
pub const EFFECTS: &[EffectDef] = &[
|
||||
fx("speed", 3402751, B),
|
||||
fx("slowness", 9154528, H),
|
||||
fx("haste", 14270531, B),
|
||||
fx("mining_fatigue", 4866583, H),
|
||||
fx("strength", 16762624, B),
|
||||
EffectDef {
|
||||
id: "instant_health",
|
||||
color: 16262179,
|
||||
category: B,
|
||||
instant: true,
|
||||
},
|
||||
EffectDef {
|
||||
id: "instant_damage",
|
||||
color: 11101546,
|
||||
category: H,
|
||||
instant: true,
|
||||
},
|
||||
fx("jump_boost", 16646020, B),
|
||||
fx("nausea", 5578058, H),
|
||||
fx("regeneration", 13458603, B),
|
||||
fx("resistance", 9520880, B),
|
||||
fx("fire_resistance", 16750848, B),
|
||||
fx("water_breathing", 10017472, B),
|
||||
fx("invisibility", 16185078, B),
|
||||
fx("blindness", 2039587, H),
|
||||
fx("night_vision", 12779366, B),
|
||||
fx("hunger", 5797459, H),
|
||||
fx("weakness", 4738376, H),
|
||||
fx("poison", 8889187, H),
|
||||
fx("wither", 7561558, H),
|
||||
fx("health_boost", 16284963, B),
|
||||
fx("absorption", 2445989, B),
|
||||
EffectDef {
|
||||
id: "saturation",
|
||||
color: 16262179,
|
||||
category: B,
|
||||
instant: true,
|
||||
},
|
||||
fx("glowing", 9740385, N),
|
||||
fx("levitation", 13565951, H),
|
||||
fx("luck", 5882118, B),
|
||||
fx("unluck", 12624973, H),
|
||||
fx("slow_falling", 15978425, B),
|
||||
fx("conduit_power", 1950417, B),
|
||||
fx("dolphins_grace", 8954814, B),
|
||||
fx("bad_omen", 745784, N),
|
||||
fx("hero_of_the_village", 4521796, B),
|
||||
fx("darkness", 2696993, H),
|
||||
fx("trial_omen", 1484454, N),
|
||||
fx("raid_omen", 14565464, N),
|
||||
fx("wind_charged", 12438015, H),
|
||||
fx("weaving", 7891290, H),
|
||||
fx("oozing", 10092451, H),
|
||||
fx("infested", 9214860, H),
|
||||
fx("breath_of_the_nautilus", 65518, B),
|
||||
];
|
||||
pub fn effect(id: &str) -> Option<&'static EffectDef> {
|
||||
let id = id.trim_start_matches("minecraft:");
|
||||
EFFECTS.iter().find(|e| e.id == id)
|
||||
}
|
||||
/// Namespaced effect ids for mob effect storage.
|
||||
pub fn namespaced(id: &'static str) -> &'static str {
|
||||
static IDS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
IDS.get_or_init(|| {
|
||||
EFFECTS
|
||||
.iter()
|
||||
.map(|e| {
|
||||
(
|
||||
e.id,
|
||||
&*Box::leak(format!("minecraft:{}", e.id).into_boxed_str()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.get(id)
|
||||
.copied()
|
||||
.unwrap_or(id)
|
||||
}
|
||||
/// A `Potion`: id, name suffix (`item.minecraft.potion.effect.<name>`) and
|
||||
/// effects as (effect, duration ticks, amplifier).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PotionDef {
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub effects: &'static [(&'static str, i32, i32)],
|
||||
}
|
||||
const fn p(
|
||||
id: &'static str,
|
||||
name: &'static str,
|
||||
effects: &'static [(&'static str, i32, i32)],
|
||||
) -> PotionDef {
|
||||
PotionDef { id, name, effects }
|
||||
}
|
||||
/// `Potions` in registration order.
|
||||
pub const POTIONS: &[PotionDef] = &[
|
||||
p("water", "water", &[]),
|
||||
p("mundane", "mundane", &[]),
|
||||
p("thick", "thick", &[]),
|
||||
p("awkward", "awkward", &[]),
|
||||
p("night_vision", "night_vision", &[("night_vision", 3600, 0)]),
|
||||
p(
|
||||
"long_night_vision",
|
||||
"night_vision",
|
||||
&[("night_vision", 9600, 0)],
|
||||
),
|
||||
p("invisibility", "invisibility", &[("invisibility", 3600, 0)]),
|
||||
p(
|
||||
"long_invisibility",
|
||||
"invisibility",
|
||||
&[("invisibility", 9600, 0)],
|
||||
),
|
||||
p("leaping", "leaping", &[("jump_boost", 3600, 0)]),
|
||||
p("long_leaping", "leaping", &[("jump_boost", 9600, 0)]),
|
||||
p("strong_leaping", "leaping", &[("jump_boost", 1800, 1)]),
|
||||
p(
|
||||
"fire_resistance",
|
||||
"fire_resistance",
|
||||
&[("fire_resistance", 3600, 0)],
|
||||
),
|
||||
p(
|
||||
"long_fire_resistance",
|
||||
"fire_resistance",
|
||||
&[("fire_resistance", 9600, 0)],
|
||||
),
|
||||
p("swiftness", "swiftness", &[("speed", 3600, 0)]),
|
||||
p("long_swiftness", "swiftness", &[("speed", 9600, 0)]),
|
||||
p("strong_swiftness", "swiftness", &[("speed", 1800, 1)]),
|
||||
p("slowness", "slowness", &[("slowness", 1800, 0)]),
|
||||
p("long_slowness", "slowness", &[("slowness", 4800, 0)]),
|
||||
p("strong_slowness", "slowness", &[("slowness", 400, 3)]),
|
||||
p(
|
||||
"turtle_master",
|
||||
"turtle_master",
|
||||
&[("slowness", 400, 3), ("resistance", 400, 2)],
|
||||
),
|
||||
p(
|
||||
"long_turtle_master",
|
||||
"turtle_master",
|
||||
&[("slowness", 800, 3), ("resistance", 800, 2)],
|
||||
),
|
||||
p(
|
||||
"strong_turtle_master",
|
||||
"turtle_master",
|
||||
&[("slowness", 400, 5), ("resistance", 400, 3)],
|
||||
),
|
||||
p(
|
||||
"water_breathing",
|
||||
"water_breathing",
|
||||
&[("water_breathing", 3600, 0)],
|
||||
),
|
||||
p(
|
||||
"long_water_breathing",
|
||||
"water_breathing",
|
||||
&[("water_breathing", 9600, 0)],
|
||||
),
|
||||
p("healing", "healing", &[("instant_health", 1, 0)]),
|
||||
p("strong_healing", "healing", &[("instant_health", 1, 1)]),
|
||||
p("harming", "harming", &[("instant_damage", 1, 0)]),
|
||||
p("strong_harming", "harming", &[("instant_damage", 1, 1)]),
|
||||
p("poison", "poison", &[("poison", 900, 0)]),
|
||||
p("long_poison", "poison", &[("poison", 1800, 0)]),
|
||||
p("strong_poison", "poison", &[("poison", 432, 1)]),
|
||||
p("regeneration", "regeneration", &[("regeneration", 900, 0)]),
|
||||
p(
|
||||
"long_regeneration",
|
||||
"regeneration",
|
||||
&[("regeneration", 1800, 0)],
|
||||
),
|
||||
p(
|
||||
"strong_regeneration",
|
||||
"regeneration",
|
||||
&[("regeneration", 450, 1)],
|
||||
),
|
||||
p("strength", "strength", &[("strength", 3600, 0)]),
|
||||
p("long_strength", "strength", &[("strength", 9600, 0)]),
|
||||
p("strong_strength", "strength", &[("strength", 1800, 1)]),
|
||||
p("weakness", "weakness", &[("weakness", 1800, 0)]),
|
||||
p("long_weakness", "weakness", &[("weakness", 4800, 0)]),
|
||||
p("luck", "luck", &[("luck", 6000, 0)]),
|
||||
p("slow_falling", "slow_falling", &[("slow_falling", 1800, 0)]),
|
||||
p(
|
||||
"long_slow_falling",
|
||||
"slow_falling",
|
||||
&[("slow_falling", 4800, 0)],
|
||||
),
|
||||
p("wind_charged", "wind_charged", &[("wind_charged", 3600, 0)]),
|
||||
p("weaving", "weaving", &[("weaving", 3600, 0)]),
|
||||
p("oozing", "oozing", &[("oozing", 3600, 0)]),
|
||||
p("infested", "infested", &[("infested", 3600, 0)]),
|
||||
];
|
||||
pub fn potion(id: &str) -> Option<&'static PotionDef> {
|
||||
let id = id.trim_start_matches("minecraft:");
|
||||
POTIONS.iter().find(|p| p.id == id)
|
||||
}
|
||||
/// `PotionContents.BASE_POTION_COLOR` (ARGB -13083194) as RGB.
|
||||
pub const BASE_POTION_COLOR: u32 = 0x385DC6;
|
||||
/// `PotionContents.getColor`: amplifier-weighted mix of visible effect colours.
|
||||
pub fn potion_color(id: Option<&str>) -> u32 {
|
||||
let Some(def) = id.and_then(potion) else {
|
||||
return BASE_POTION_COLOR;
|
||||
};
|
||||
let (mut r, mut g, mut b, mut total) = (0u32, 0u32, 0u32, 0u32);
|
||||
for (e, _, amp) in def.effects {
|
||||
let Some(e) = effect(e) else { continue };
|
||||
let w = *amp as u32 + 1;
|
||||
r += w * (e.color >> 16 & 255);
|
||||
g += w * (e.color >> 8 & 255);
|
||||
b += w * (e.color & 255);
|
||||
total += w;
|
||||
}
|
||||
match (
|
||||
r.checked_div(total),
|
||||
g.checked_div(total),
|
||||
b.checked_div(total),
|
||||
) {
|
||||
(Some(r), Some(g), Some(b)) => (r << 16) | (g << 8) | b,
|
||||
_ => BASE_POTION_COLOR,
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
//! Registry contents and source state that commands validate and complete
|
||||
//! arguments against (what the server's registry access holds).
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Registry contents and source state used to validate and complete arguments.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Registries {
|
||||
/// Permission level of the command source.
|
||||
pub permission: u8,
|
||||
pub items: Vec<String>,
|
||||
pub item_tags: Vec<String>,
|
||||
/// Block id -> (property, allowed values).
|
||||
pub blocks: BTreeMap<String, Vec<(String, Vec<String>)>>,
|
||||
pub block_tags: Vec<String>,
|
||||
/// Entity types that `summon` can create.
|
||||
pub summonable: Vec<String>,
|
||||
/// Every entity type (selector `type=`).
|
||||
pub entity_types: Vec<String>,
|
||||
pub effects: Vec<String>,
|
||||
pub enchantments: Vec<String>,
|
||||
pub structures: Vec<String>,
|
||||
pub biomes: Vec<String>,
|
||||
pub dimensions: Vec<String>,
|
||||
pub clocks: Vec<String>,
|
||||
pub timelines: Vec<String>,
|
||||
pub time_markers: Vec<String>,
|
||||
pub advancements: Vec<String>,
|
||||
/// Game rule name -> integer-valued.
|
||||
pub game_rules: Vec<(String, bool)>,
|
||||
pub players: Vec<String>,
|
||||
/// Scoreboard objective names, team names and tracked score holders.
|
||||
pub objectives: Vec<String>,
|
||||
pub teams: Vec<String>,
|
||||
pub tracked: Vec<String>,
|
||||
/// Loaded datapack functions (`ns:path`) and function tags (`ns:path`).
|
||||
pub functions: Vec<String>,
|
||||
/// `/data ... storage` ids.
|
||||
pub storages: Vec<String>,
|
||||
/// Boss bar ids and datapack predicate ids.
|
||||
pub bossbars: Vec<String>,
|
||||
pub predicates: Vec<String>,
|
||||
pub item_modifiers: Vec<String>,
|
||||
pub schedules: Vec<String>,
|
||||
pub stopwatches: Vec<String>,
|
||||
pub recipes: Vec<String>,
|
||||
pub sounds: Vec<String>,
|
||||
pub damage_types: Vec<String>,
|
||||
pub attributes: Vec<String>,
|
||||
pub loot_tables: Vec<String>,
|
||||
pub templates: Vec<String>,
|
||||
pub random_sequences: Vec<String>,
|
||||
pub datapacks_enabled: Vec<String>,
|
||||
pub datapacks_disabled: Vec<String>,
|
||||
pub function_tags: Vec<String>,
|
||||
/// The block the source is looking at (relevant coordinates).
|
||||
pub target_block: Option<[i32; 3]>,
|
||||
/// Every registry's element and tag ids (`minecraft:item` -> ...), as
|
||||
/// the server's registry access and reloadable registries hold them.
|
||||
pub by_registry: BTreeMap<String, RegistryIds>,
|
||||
/// Blocks whose default state has a block entity.
|
||||
pub block_entities: std::collections::BTreeSet<String>,
|
||||
/// Block tag -> member blocks.
|
||||
pub block_tag_members: BTreeMap<String, Vec<String>>,
|
||||
/// Data component types with a codec (not transient).
|
||||
pub persistent_components: Vec<String>,
|
||||
/// Recipes `/recipe` can give (not special).
|
||||
pub learnable_recipes: Vec<String>,
|
||||
/// Advancement id -> criterion names.
|
||||
pub advancement_criteria: BTreeMap<String, Vec<String>>,
|
||||
/// World clock -> time markers shown in commands.
|
||||
pub clock_time_markers: BTreeMap<String, Vec<String>>,
|
||||
/// Trigger objectives the source may use (`/trigger` suggestions).
|
||||
pub trigger_objectives: Vec<String>,
|
||||
/// Objectives with the `trigger` criterion -> holder -> whether its score is locked.
|
||||
pub trigger_scores: BTreeMap<String, BTreeMap<String, bool>>,
|
||||
}
|
||||
/// The ids and tag ids of one registry.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RegistryIds {
|
||||
pub ids: Vec<String>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
const EMPTY: &[String] = &[];
|
||||
impl Registries {
|
||||
/// Fill what the game did not build itself from vanilla's exported
|
||||
/// registries (`command-registries.json`, written by
|
||||
/// `VanillaCommandParseOracle registries`): built-in registries such as
|
||||
/// mob effects, data component types and dialogs, block entities and tag
|
||||
/// members. Registries that data packs reload stay with the game.
|
||||
pub fn fill_from_export(&mut self, r: &serde_json::Value) {
|
||||
fn strings(v: &serde_json::Value) -> Vec<String> {
|
||||
v.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|x| x.as_str().map(str::to_owned))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
let fill = |field: &mut Vec<String>, key: &str| {
|
||||
if field.is_empty() {
|
||||
*field = strings(&r[key]);
|
||||
}
|
||||
};
|
||||
fill(&mut self.effects, "effects");
|
||||
fill(&mut self.enchantments, "enchantments");
|
||||
fill(&mut self.attributes, "attributes");
|
||||
fill(&mut self.damage_types, "damage_types");
|
||||
fill(&mut self.templates, "templates");
|
||||
fill(&mut self.persistent_components, "persistent_components");
|
||||
fill(&mut self.learnable_recipes, "learnable_recipes");
|
||||
const RELOADED: [&str; 5] = [
|
||||
"minecraft:advancement",
|
||||
"minecraft:recipe",
|
||||
"minecraft:loot_table",
|
||||
"minecraft:predicate",
|
||||
"minecraft:item_modifier",
|
||||
];
|
||||
for (name, entry) in r["by_registry"].as_object().into_iter().flatten() {
|
||||
if RELOADED.contains(&name.as_str()) || self.by_registry.contains_key(name) {
|
||||
continue;
|
||||
}
|
||||
self.by_registry.insert(
|
||||
name.clone(),
|
||||
RegistryIds {
|
||||
ids: strings(&entry["ids"]),
|
||||
tags: strings(&entry["tags"]),
|
||||
},
|
||||
);
|
||||
}
|
||||
if self.block_entities.is_empty() {
|
||||
self.block_entities = strings(&r["block_entities"]).into_iter().collect();
|
||||
}
|
||||
if self.block_tag_members.is_empty() {
|
||||
for (tag, members) in r["block_tag_members"].as_object().into_iter().flatten() {
|
||||
self.block_tag_members.insert(tag.clone(), strings(members));
|
||||
}
|
||||
}
|
||||
if self.clock_time_markers.is_empty() {
|
||||
for (clock, markers) in r["clock_time_markers"].as_object().into_iter().flatten() {
|
||||
self.clock_time_markers
|
||||
.insert(clock.clone(), strings(markers));
|
||||
}
|
||||
}
|
||||
if self.advancement_criteria.is_empty() {
|
||||
for (id, criteria) in r["advancement_criteria"].as_object().into_iter().flatten() {
|
||||
self.advancement_criteria
|
||||
.insert(id.clone(), strings(criteria));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The element ids of a registry (sorted).
|
||||
pub fn ids(&self, registry: &str) -> &[String] {
|
||||
if let Some(r) = self.by_registry.get(registry) {
|
||||
return &r.ids;
|
||||
}
|
||||
match registry {
|
||||
"minecraft:item" => &self.items,
|
||||
"minecraft:entity_type" => &self.entity_types,
|
||||
"minecraft:mob_effect" => &self.effects,
|
||||
"minecraft:enchantment" => &self.enchantments,
|
||||
"minecraft:worldgen/structure" => &self.structures,
|
||||
"minecraft:worldgen/biome" => &self.biomes,
|
||||
"minecraft:world_clock" => &self.clocks,
|
||||
"minecraft:timeline" => &self.timelines,
|
||||
"minecraft:attribute" => &self.attributes,
|
||||
"minecraft:damage_type" => &self.damage_types,
|
||||
"minecraft:sound_event" => &self.sounds,
|
||||
"minecraft:predicate" => &self.predicates,
|
||||
"minecraft:item_modifier" => &self.item_modifiers,
|
||||
"minecraft:loot_table" => &self.loot_tables,
|
||||
"minecraft:recipe" => &self.recipes,
|
||||
"minecraft:advancement" => &self.advancements,
|
||||
_ => EMPTY,
|
||||
}
|
||||
}
|
||||
/// The tag ids of a registry (sorted).
|
||||
pub fn tags(&self, registry: &str) -> &[String] {
|
||||
if let Some(r) = self.by_registry.get(registry) {
|
||||
return &r.tags;
|
||||
}
|
||||
match registry {
|
||||
"minecraft:item" => &self.item_tags,
|
||||
"minecraft:block" => &self.block_tags,
|
||||
_ => EMPTY,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//! Sound sources (`SoundSource`), by name.
|
||||
|
||||
/// SoundSource names in vanilla order.
|
||||
pub const SOUND_SOURCES: [&str; 11] = [
|
||||
"master", "music", "record", "weather", "block", "hostile", "neutral", "player", "ambient",
|
||||
"voice", "ui",
|
||||
];
|
||||
@@ -1,24 +0,0 @@
|
||||
//! UUIDs as vanilla's four-int form, to and from their text form.
|
||||
|
||||
pub fn uuid_text(u: [i32; 4]) -> String {
|
||||
let hex: String = u.iter().map(|v| format!("{:08x}", *v as u32)).collect();
|
||||
format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
&hex[0..8],
|
||||
&hex[8..12],
|
||||
&hex[12..16],
|
||||
&hex[16..20],
|
||||
&hex[20..32]
|
||||
)
|
||||
}
|
||||
pub fn uuid_from_text(s: &str) -> Option<[i32; 4]> {
|
||||
let hex: String = s.chars().filter(|c| *c != '-').collect();
|
||||
if hex.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
let mut out = [0i32; 4];
|
||||
for (i, v) in out.iter_mut().enumerate() {
|
||||
*v = u32::from_str_radix(&hex[i * 8..i * 8 + 8], 16).ok()? as i32;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//! Who an action came from or is aimed at: the player or a mob by id.
|
||||
|
||||
/// Who caused damage or is being targeted.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Who {
|
||||
Player,
|
||||
Mob(u32),
|
||||
}
|
||||
@@ -9,103 +9,55 @@ description = "Horizon native development client"
|
||||
|
||||
[dependencies]
|
||||
horizon-assets = { path = "../horizon-assets" }
|
||||
horizon-client-core = { path = "../horizon-client-core" }
|
||||
horizon-command = { path = "../horizon-command" }
|
||||
horizon-items = { path = "../horizon-items" }
|
||||
horizon-core = { path = "../horizon-core" }
|
||||
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
|
||||
horizon-threads = { path = "../horizon-threads" }
|
||||
horizon-hdr = { path = "../horizon-hdr" }
|
||||
horizon-world = { path = "../horizon-world" }
|
||||
horizon-protocol = { path = "../horizon-protocol" }
|
||||
# server.properties, eula.txt and the user lists, shared with horizon-server.
|
||||
horizon-server-files = { path = "../horizon-server-files" }
|
||||
# Shader packs: reading them and turning their GLSL into naga modules.
|
||||
horizon-shaders = { path = "../horizon-shaders" }
|
||||
# Horizon's own shader packs: the format, the stages and the check (docs/shaders.md).
|
||||
horizon-shader-api = { path = "../horizon-shader-api" }
|
||||
horizon-friends = { path = "../horizon-friends" }
|
||||
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
|
||||
# Their runtime is the `mods` feature.
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
|
||||
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl", "naga-ir"] }
|
||||
horizon-mods = { path = "../horizon-mods" }
|
||||
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl"] }
|
||||
winit = "0.30.13"
|
||||
pollster = "0.4"
|
||||
bytemuck = { version = "1.24", features = ["derive"] }
|
||||
glam = "0.30"
|
||||
png = "0.18"
|
||||
sha2 = "0.10.9"
|
||||
# Server resource packs are checked and cached by their SHA-1, as vanilla does.
|
||||
sha1 = "0.10"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1.0.151", features = ["raw_value"] }
|
||||
# hlsl-out: the gfx.rs tests translate every shader as wgpu's DirectX 12 backend does. A normal feature,
|
||||
# not a dev-dependency's, so builds, tests and examples share one build of naga, wgpu and this crate.
|
||||
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
|
||||
# Sound (ALSA on Linux); the `audio` feature.
|
||||
cpal = { version = "0.16", optional = true }
|
||||
# Shader packs: pack.toml, and WGSL checked before it reaches wgpu.
|
||||
toml = "1.1"
|
||||
naga = { version = "30.0.1", features = ["wgsl-in"] }
|
||||
# Java (OptiFine/Iris) shader packs, translated to WGSL.
|
||||
horizon-shaderpack = { path = "../horizon-shaderpack" }
|
||||
cpal = "0.16"
|
||||
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
|
||||
miniz_oxide = "0.8"
|
||||
ab_glyph = "0.2.32"
|
||||
log = "0.4"
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
|
||||
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
|
||||
flate2 = { version = "1.1.10", default-features = false, features = ["rust_backend"] }
|
||||
horizon-theme = { path = "../horizon-theme" }
|
||||
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md); `live` is
|
||||
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`).
|
||||
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
|
||||
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md).
|
||||
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
|
||||
horizon-bench = { path = "../horizon-bench" }
|
||||
arboard = { version = "3.6.1", default-features = false, features = ["wayland-data-control"] }
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input); the `controllers` feature.
|
||||
gilrs = { version = "0.11", optional = true }
|
||||
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input).
|
||||
gilrs = "0.11"
|
||||
# The DualSense's rumble, adaptive triggers and lights over HID (gilrs only reads it).
|
||||
horizon-dualsense = { path = "../horizon-dualsense", optional = true }
|
||||
horizon-dualsense = { path = "../horizon-dualsense" }
|
||||
# HTTPS to the Minecraft services (online-mode session join).
|
||||
ureq = { version = "2", default-features = false, features = ["tls"] }
|
||||
unicode-bidi = "0.3.18"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
# The server thread's CPU time per tick (`HORIZON_SPIKES`, spans.rs).
|
||||
rustix = { version = "1", default-features = false, features = ["std", "time"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
# DirectX 12, where Vulkan has no adapter or device, or by choice (`graphicsApi`, graphics.rs).
|
||||
# Shaders compile with FXC (d3dcompiler_47.dll, part of Windows), or DXC when dxcompiler.dll is
|
||||
# on the PATH.
|
||||
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
# OpenGL (WGL on Windows, EGL on Linux), the last fallback, for GPUs with no Vulkan or
|
||||
# DirectX 12 driver (`graphicsApi`, graphics.rs).
|
||||
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
# The icon resource in the .exe (build.rs).
|
||||
winresource = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["audio", "controllers"]
|
||||
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
|
||||
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
|
||||
audio = ["dep:cpal"]
|
||||
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
|
||||
controllers = ["dep:gilrs", "dep:horizon-dualsense"]
|
||||
# Runs code mods (wasmtime and cranelift: a quarter of a clean build). On in every release build
|
||||
# (release.yml); the tests and probes don't need it. Content mods load without it.
|
||||
mods = ["horizon-mods/runtime"]
|
||||
# Records the call sites of mob random draws (`mob_ai_probe` traces).
|
||||
rng-trace = ["horizon-core/rng-trace"]
|
||||
|
||||
# Its own name, not `probe`: Cargo copies every crate's examples into one target/<profile>/examples.
|
||||
[[example]]
|
||||
name = "client_probe"
|
||||
path = "examples/probe/main.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
horizon-items = { path = "../horizon-items", features = ["test-support"] }
|
||||
# Every WGSL shader translated as wgpu's DirectX 12 backend does (gfx.rs tests).
|
||||
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out", "msl-out", "spv-out"] }
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Controller",
|
||||
"horizon.options.vibration": "Vibration",
|
||||
"horizon.options.adaptive_triggers": "Adaptive Triggers",
|
||||
"horizon.options.lightbar": "Lightbar",
|
||||
"horizon.options.look_sensitivity": "Look Sensitivity",
|
||||
"horizon.options.invert_x": "Invert Look X",
|
||||
"horizon.options.invert_y": "Invert Look Y",
|
||||
"horizon.options.sneak_button": "Sneak Button",
|
||||
"horizon.options.move_deadzone": "Move Deadzone",
|
||||
"horizon.options.look_deadzone": "Look Deadzone",
|
||||
"horizon.options.button_glyphs": "Button Glyphs",
|
||||
"horizon.options.menu_navigation": "Menu Navigation",
|
||||
"horizon.options.on_screen_keyboard": "On-Screen Keyboard"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Геймпад",
|
||||
"horizon.options.vibration": "Вибрация",
|
||||
"horizon.options.adaptive_triggers": "Адаптивные триггеры",
|
||||
"horizon.options.lightbar": "Подсветка",
|
||||
"horizon.options.look_sensitivity": "Чувствительность обзора",
|
||||
"horizon.options.invert_x": "Инверсия обзора по X",
|
||||
"horizon.options.invert_y": "Инверсия обзора по Y",
|
||||
"horizon.options.sneak_button": "Кнопка приседания",
|
||||
"horizon.options.move_deadzone": "Мёртвая зона движения",
|
||||
"horizon.options.look_deadzone": "Мёртвая зона обзора",
|
||||
"horizon.options.button_glyphs": "Символы кнопок",
|
||||
"horizon.options.menu_navigation": "Навигация по меню",
|
||||
"horizon.options.on_screen_keyboard": "Экранная клавиатура"
|
||||
}
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
//! Replays `tools/test_ambient_sound_parity.py` scripts through Horizon's
|
||||
//! ambient sound handlers over its `SoundEngine`:
|
||||
//! `ambient_fixture ROOT INPUT OUTPUT`.
|
||||
use crate::audio_common as common;
|
||||
#[path = "audio_common/mod.rs"]
|
||||
mod common;
|
||||
use common::{Fake, d, f, flush};
|
||||
use horizon_client::ambience::{Ambience, AmbientContext, AmbientSounds, Mood, bubble_column_in};
|
||||
use horizon_client::sound_engine::{Bound, SoundEngine, TickContext, Ticking};
|
||||
@@ -46,8 +47,8 @@ fn sounds(a: &[&str]) -> AmbientSounds {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
|
||||
engine.listening = true;
|
||||
let mut backend = Fake::default();
|
||||
+4
-7
@@ -50,20 +50,19 @@ fn particle(k: ArrowParticle) -> &'static str {
|
||||
ArrowParticle::Splash => "splash",
|
||||
}
|
||||
}
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
if args.len() != 3 {
|
||||
return Err("usage: arrow_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into());
|
||||
}
|
||||
let root = PathBuf::from(&args[0]);
|
||||
let dir = PathBuf::from(&args[1]);
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
let mut scene = World::test_scene(&assets, &render, 1)?;
|
||||
let mut scene = World::test_scene(&assets, 1)?;
|
||||
fs::write(
|
||||
dir.join("scene.json"),
|
||||
serde_json::to_vec(
|
||||
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":render.model_gallery}),
|
||||
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":assets.model_gallery}),
|
||||
)?,
|
||||
)?;
|
||||
if args[2] == "--describe" {
|
||||
@@ -163,8 +162,6 @@ pub fn main() -> Result<()> {
|
||||
ArrowSound::HitGround => "hit",
|
||||
ArrowSound::Splash => "splash",
|
||||
ArrowSound::Extinguish => "extinguish",
|
||||
ArrowSound::TridentHitGround => "trident_hit_ground",
|
||||
ArrowSound::TridentHit => "trident_hit",
|
||||
},
|
||||
volume.to_bits(),
|
||||
pitch.to_bits()
|
||||
+2
-2
@@ -18,8 +18,8 @@ fn music(m: Option<&Music>) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let root = std::path::Path::new(&args[1]);
|
||||
let tables = MusicTables::load(root);
|
||||
let ambient = AmbientTables::load(root);
|
||||
File renamed without changes.
+4
-5
@@ -24,21 +24,20 @@ fn db(v: &Value) -> f64 {
|
||||
f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap())
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let [root, input, output, scene_json] = &args[..] else {
|
||||
return Err("usage: auto_jump_probe RESOURCES INPUT OUTPUT SCENE_JSON".into());
|
||||
};
|
||||
let root = PathBuf::from(root);
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
let mut scene = World::test_scene(&assets, &render, 1)?;
|
||||
let mut scene = World::test_scene(&assets, 1)?;
|
||||
let binding = collisions::load(&root, assets.registry)?;
|
||||
let resources = MotionResources::load(&root, &assets)?;
|
||||
fs::write(
|
||||
scene_json,
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"radius": 1, "resident_chunks": scene.chunks.len(), "model_gallery": render.model_gallery,
|
||||
"radius": 1, "resident_chunks": scene.chunks.len(), "model_gallery": assets.model_gallery,
|
||||
}))?,
|
||||
)?;
|
||||
let mut out = BufWriter::new(fs::File::create(output)?);
|
||||
+2
-2
@@ -118,8 +118,8 @@ fn status(s: ShulkerStatus) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: block_entity_anim_fixture RESOURCES INPUTS OUTPUT".into());
|
||||
};
|
||||
+2
-3
@@ -110,8 +110,8 @@ fn intern(s: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, textures, output] = args.as_slice() else {
|
||||
return Err("usage: block_entity_render_fixture RESOURCES INPUTS TEXTURES OUTPUT".into());
|
||||
};
|
||||
@@ -367,7 +367,6 @@ pub fn main() -> Result<()> {
|
||||
lid: &|_| open,
|
||||
sign: &|_| sign.as_ref(),
|
||||
banner: &|_| layers.clone(),
|
||||
skull: &|_| None,
|
||||
glyph: &|c| atlas.glyph_cell(c),
|
||||
};
|
||||
let mut verts = Vec::new();
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Full production fact loading, including file reads, interning and temporary destruction.
|
||||
//! cargo run --profile fast -p horizon-client --example block_facts_bench -- ROOT [SAMPLES]
|
||||
//! Use SAMPLES=1 with an OS peak-RSS wrapper; `digest` checks all facts
|
||||
//! and the shape sharing partition (including sharing between outlines and edges).
|
||||
use horizon_client::blocks::BlockFacts;
|
||||
use horizon_world::{DATA_VERSION, nbt::Tag, registry::BlockRegistry};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{HashMap, hash_map::DefaultHasher},
|
||||
fs,
|
||||
hash::{Hash, Hasher},
|
||||
hint::black_box,
|
||||
path::Path,
|
||||
time::Instant,
|
||||
};
|
||||
type Result<T> = horizon_client::Result<T>;
|
||||
|
||||
fn tag(value: &Value) -> Result<Tag> {
|
||||
Ok(match value {
|
||||
Value::String(s) => Tag::String(s.as_str().into()),
|
||||
Value::Number(n) => Tag::Int(i32::try_from(n.as_i64().ok_or("registry number")?)?),
|
||||
Value::Array(a) => Tag::List(a.iter().map(tag).collect::<Result<_>>()?),
|
||||
Value::Object(o) => Tag::Compound(
|
||||
o.iter()
|
||||
.map(|(k, v)| Ok((k.as_str().into(), tag(v)?)))
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
_ => return Err("registry value".into()),
|
||||
})
|
||||
}
|
||||
fn registry(root: &Path) -> Result<BlockRegistry> {
|
||||
let facts: Value = serde_json::from_slice(&fs::read(root.join("facts.json"))?)?;
|
||||
Ok(BlockRegistry::from_nbt(&Tag::compound([
|
||||
("DataVersion", Tag::Int(DATA_VERSION)),
|
||||
(
|
||||
"states",
|
||||
Tag::List(
|
||||
facts["states"]
|
||||
.as_array()
|
||||
.ok_or("registry states")?
|
||||
.iter()
|
||||
.map(|s| tag(&s[4]))
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
),
|
||||
("defaults", tag(&facts["defaults"])?),
|
||||
]))?)
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let root = Path::new(
|
||||
args.first()
|
||||
.ok_or("usage: block_facts_bench ROOT [SAMPLES|digest]")?,
|
||||
);
|
||||
let setup = Instant::now();
|
||||
let registry = registry(root)?;
|
||||
println!(
|
||||
"registry_setup_ms={:.3}",
|
||||
setup.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
if args.get(1).is_some_and(|s| s == "digest") {
|
||||
let facts = BlockFacts::load(root, ®istry)?;
|
||||
let mut hash = DefaultHasher::new();
|
||||
let mut shapes = HashMap::new();
|
||||
facts.air.hash(&mut hash);
|
||||
for state in &facts.states {
|
||||
format!("{state:?}").hash(&mut hash);
|
||||
for shape in [&state.outline, &state.outline_edges] {
|
||||
let next = shapes.len();
|
||||
shapes
|
||||
.entry((shape.as_ptr(), shape.len()))
|
||||
.or_insert(next)
|
||||
.hash(&mut hash);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"states={} shared_shapes={} digest={:016x}",
|
||||
facts.states.len(),
|
||||
shapes.len(),
|
||||
hash.finish()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let samples: usize = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
|
||||
assert!(samples > 0);
|
||||
// First load is reported separately; OS file caches are not forcibly evicted.
|
||||
let load = || -> Result<f64> {
|
||||
let start = Instant::now();
|
||||
let facts = black_box(BlockFacts::load(black_box(root), black_box(®istry))?);
|
||||
let ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
drop(facts);
|
||||
Ok(ms)
|
||||
};
|
||||
println!("first_load_ms={:.3}", load()?);
|
||||
if samples > 1 {
|
||||
load()?;
|
||||
load()?;
|
||||
let mut times = (0..samples).map(|_| load()).collect::<Result<Vec<_>>>()?;
|
||||
println!("samples_ms={times:.3?}");
|
||||
times.sort_by(f64::total_cmp);
|
||||
println!("median_ms={:.3}", times[times.len() / 2]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+2
-4
@@ -8,15 +8,13 @@ use std::{
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::Path,
|
||||
};
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let root = Path::new(&args[1]);
|
||||
let assets = Assets::load(root)?;
|
||||
let render = assets.reload(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let tables = MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(root, assets.registry)?,
|
||||
)?;
|
||||
+2
-2
@@ -51,8 +51,8 @@ impl Around for Column {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: block_transform_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+5
-9
@@ -14,17 +14,13 @@ use std::{
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::Path,
|
||||
};
|
||||
/// The stage sprite origin the cracks carry (the oracle compares the geometry and UVs only).
|
||||
const STAGE: [f32; 2] = [0.25, 0.5];
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let root = Path::new(&args[1]);
|
||||
let assets = Assets::load(root)?;
|
||||
let render = assets.reload(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let tables = MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(root, assets.registry)?,
|
||||
)?;
|
||||
@@ -37,7 +33,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut vertices = Vec::new();
|
||||
let mut translucent = false;
|
||||
if facts.get(state).render_shape == RenderShape::Model {
|
||||
let models = render
|
||||
let models = assets
|
||||
.model_catalog
|
||||
.collect(state, BlockPos::new(p[0], p[1], p[2]))?;
|
||||
let offset = tables.offset(state, p[0], p[2]).unwrap_or([0.0; 3]);
|
||||
@@ -49,7 +45,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.iter()
|
||||
.any(|(_, m)| m.faces.iter().any(|f| f.translucent));
|
||||
for (_, m) in models {
|
||||
breaking::geometry(m, origin, translucent, STAGE, &mut vertices);
|
||||
breaking::geometry(m, origin, translucent, &mut vertices);
|
||||
}
|
||||
}
|
||||
let quads: Vec<_> = vertices
|
||||
@@ -60,7 +56,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
q.iter()
|
||||
.map(|v| {
|
||||
assert_eq!(v.color, 0xffff_ffff);
|
||||
assert_eq!(v.light, breaking::stage_light(STAGE));
|
||||
assert_eq!(v.light, 240 | (240 << 8));
|
||||
assert_eq!(v.layer, if translucent { 12 } else { 11 });
|
||||
[v.pos[0], v.pos[1], v.pos[2], v.uv[0], v.uv[1]]
|
||||
})
|
||||
+2
-2
@@ -323,8 +323,8 @@ fn avatar(c: &Value, out: &mut impl Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-9
@@ -70,8 +70,8 @@ fn describe(state: &BlockState) -> String {
|
||||
format!("{}|{}", state.name, props.join(","))
|
||||
}
|
||||
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: cauldron_fixture RESOURCES INPUT OUTPUT")?,
|
||||
@@ -82,19 +82,12 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
let brewing = horizon_client::brewing::Brewing::load(root, &items);
|
||||
// The inventory menu never reads banner patterns.
|
||||
let banners = horizon_client::banner::Banners::default();
|
||||
let station_recipes = horizon_client::stations::StationRecipes::default();
|
||||
let enchantments = horizon_client::enchant::Enchantments::default();
|
||||
let names = horizon_client::creative::Names::default();
|
||||
let env = Env {
|
||||
items: &items,
|
||||
recipes: &recipes,
|
||||
brewing: &brewing,
|
||||
banners: &banners,
|
||||
station_recipes: &station_recipes,
|
||||
enchantments: &enchantments,
|
||||
names: &names,
|
||||
trade: None,
|
||||
maps: None,
|
||||
};
|
||||
let mut out = String::new();
|
||||
for line in input.lines() {
|
||||
+6
-5
@@ -5,9 +5,8 @@
|
||||
//! the oracle's JSON form.
|
||||
//!
|
||||
//! usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT
|
||||
use crate::pack_screen_fixture;
|
||||
use horizon_client::{
|
||||
chat::{ChatAbilities, ChatAction, ChatScreen, ChatVisibility, Commands},
|
||||
chat::{ChatAction, ChatScreen, Commands},
|
||||
command::vanilla,
|
||||
hud::HudAtlas,
|
||||
lang,
|
||||
@@ -16,11 +15,14 @@ use horizon_client::{
|
||||
use serde_json::{Value, json};
|
||||
use std::{fmt::Write as _, fs, path::PathBuf};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
#[path = "pack_screen_fixture.rs"]
|
||||
#[allow(dead_code)]
|
||||
mod pack_screen_fixture;
|
||||
#[path = "chat_screen_fixture/registries.rs"]
|
||||
mod registries;
|
||||
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, registries, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT".into());
|
||||
};
|
||||
@@ -53,7 +55,6 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
history.len(),
|
||||
w,
|
||||
h,
|
||||
ChatAbilities::new(ChatVisibility::Full),
|
||||
);
|
||||
screen.auto_suggestions = case["autoSuggest"] == true;
|
||||
let mut closed = false;
|
||||
File renamed without changes.
Loaded 100 of 2324 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user