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
@@ -1,17 +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
|
||||
/tools/release_channels.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 +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
|
||||
+38
-19
@@ -1,29 +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. 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,17 +56,42 @@ jobs:
|
||||
run: |
|
||||
cargo +stable fmt --all -- --check
|
||||
cargo +stable clippy --workspace --all-targets -- -D warnings
|
||||
# The server build (released as horizon-engine): the game without sound or controllers.
|
||||
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
|
||||
- name: Test Rust core and voxel kernel correctness
|
||||
if: steps.rust.outputs.required != 'false'
|
||||
run: cargo +stable test --workspace
|
||||
parity:
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/parity.yml
|
||||
with:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
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
|
||||
# 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:
|
||||
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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -23,10 +23,6 @@ permissions:
|
||||
concurrency:
|
||||
group: nightly-audit
|
||||
cancel-in-progress: false
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: [self-hosted, horizon-game]
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
name: 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). 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
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
|
||||
HORIZON_TELEMETRY_URL: "off"
|
||||
|
||||
jobs:
|
||||
parity:
|
||||
runs-on: [self-hosted, horizon-game]
|
||||
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: ${{ inputs.head }}
|
||||
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: 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: ${{ inputs.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
|
||||
@@ -1,55 +0,0 @@
|
||||
name: promote
|
||||
# Beta game releases become stable once a day (docs/release-flow.md, "Channels"): at 20:00 UTC (21:00 in Paris in
|
||||
# winter time, 22:00 in summer) tools/release_channels.py promotes the newest beta that had been out 6 hours by then,
|
||||
# passed its release smoke test and isn't covered by an issue labelled release-blocker. Launchers on the Alpha
|
||||
# channel install every beta when it is published; Stable gets the day's promotion. The later runs of the evening are
|
||||
# retries for a run GitHub starts late or skips: they promote the same beta, so stable still moves once.
|
||||
# By hand, from GitHub's app too (Actions > promote > Run workflow):
|
||||
# gh workflow run promote -f tag=v0.2.3 # that beta now, without waiting for 20:00
|
||||
# gh workflow run promote -f tag=v0.2.3 -f force=true # even blocked or without its smoke test
|
||||
# gh workflow run promote -f dry_run=true # say what would be promoted
|
||||
# Holding betas back: label an issue release-blocker. While it is open it holds every beta; closed as completed it
|
||||
# still holds the betas published before it closed. The issue's text is never read (its author could edit it).
|
||||
# When the oldest waiting beta is 30 hours old (it missed a promotion it was due for), an issue "Promotion to stable
|
||||
# has stalled" says so; it closes itself after the next promotion.
|
||||
on:
|
||||
schedule:
|
||||
- cron: '7,37 20-21 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: The beta to promote now (empty = the newest beta that is due)
|
||||
type: string
|
||||
default: ''
|
||||
force:
|
||||
description: Promote that tag even if a release-blocker covers it or its smoke test didn't run
|
||||
type: boolean
|
||||
default: false
|
||||
dry_run:
|
||||
description: Say what would be promoted, change nothing
|
||||
type: boolean
|
||||
default: false
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
concurrency:
|
||||
group: promote
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The notes of a promoted release cover every pull request since the last stable one.
|
||||
fetch-depth: 0
|
||||
# gh uses GH_TOKEN below; nothing needs git credentials on disk.
|
||||
persist-credentials: false
|
||||
- name: Promote
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
FORCE: ${{ inputs.force && 'yes' || '' }}
|
||||
DRY_RUN: ${{ (inputs.dry_run || github.ref != 'refs/heads/main') && 'yes' || '' }}
|
||||
run: python3 tools/release_channels.py promote ${TAG:+--tag "$TAG"} ${FORCE:+--force} ${DRY_RUN:+--dry-run}
|
||||
+12
-153
@@ -1,16 +1,12 @@
|
||||
name: release
|
||||
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
|
||||
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides:
|
||||
# a release when pull requests merged since the last one changed what players run and the newest merged 5 minutes
|
||||
# ago (the oldest 20 minutes ago, if merges never pause), or at once for a pull request labelled release-now.
|
||||
# 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 a beta (a pre-release) that promote.yml makes
|
||||
# stable in its daily promotion. 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:
|
||||
@@ -45,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:
|
||||
@@ -66,9 +61,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -84,9 +76,6 @@ jobs:
|
||||
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
|
||||
@@ -99,13 +88,9 @@ jobs:
|
||||
dir: horizon-macos-arm64
|
||||
asset: horizon-macos-arm64.zip
|
||||
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
|
||||
# Notarizing the zip can take a few minutes.
|
||||
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
|
||||
timeout-minutes: 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: ${{ needs.decide.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -114,11 +99,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 (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
|
||||
# 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'
|
||||
@@ -134,22 +116,6 @@ jobs:
|
||||
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 != ''
|
||||
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 != ''
|
||||
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.
|
||||
@@ -169,13 +135,9 @@ jobs:
|
||||
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'
|
||||
# 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
|
||||
"$bin/horizon-server${{ matrix.exe }}" --version
|
||||
- name: Tools from the runner
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: bash tools/ci/runner-env.sh
|
||||
@@ -203,15 +165,6 @@ jobs:
|
||||
or not report.get("meshed_sections") or not report.get("player"):
|
||||
sys.exit("the smoke run did not draw a world")
|
||||
EOF
|
||||
# Published with the release: a beta becomes stable only with it (tools/release_channels.py).
|
||||
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: runner.environment == 'self-hosted'
|
||||
with:
|
||||
name: smoke-test.json
|
||||
path: ${{ runner.temp }}/smoke-test.json
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: Package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -221,102 +174,20 @@ jobs:
|
||||
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 ;;
|
||||
else ditto -c -k --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"; 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 != ''
|
||||
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).
|
||||
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
|
||||
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
|
||||
folder="dist/${{ matrix.server_dir }}"
|
||||
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
|
||||
"$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 != ''
|
||||
run: packaging/macos/keychain.sh lock
|
||||
|
||||
sign:
|
||||
publish:
|
||||
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-*
|
||||
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-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.decide.outputs.dry_run == 'true'
|
||||
with:
|
||||
name: ephemeral.pub
|
||||
path: dist/ephemeral.pub
|
||||
retention-days: 3
|
||||
|
||||
publish:
|
||||
needs: [decide, build, sign]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -334,27 +205,15 @@ 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: 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)
|
||||
smoke=(); [[ -f dist/smoke-test.json ]] && smoke=(dist/smoke-test.json)
|
||||
# A beta (pre-release), the Alpha channel's: promote.yml makes it stable and latest in its daily promotion
|
||||
# (docs/release-flow.md).
|
||||
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
|
||||
--notes-file notes/body.md --prerelease --latest=false dist/horizon-linux-x86_64.tar.gz \
|
||||
dist/horizon-windows-x64.zip dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz \
|
||||
dist/SHA256SUMS "${signature[@]}" "${smoke[@]}"
|
||||
--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: |
|
||||
|
||||
@@ -1,24 +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 }}
|
||||
@@ -53,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
|
||||
@@ -71,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
+2
-37
@@ -1293,7 +1293,6 @@ dependencies = [
|
||||
"log",
|
||||
"presser",
|
||||
"thiserror 2.0.21",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1395,7 +1394,6 @@ dependencies = [
|
||||
name = "horizon-bench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"horizon-telemetry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -1431,14 +1429,12 @@ dependencies = [
|
||||
"horizon-shaderpack",
|
||||
"horizon-telemetry",
|
||||
"horizon-theme",
|
||||
"horizon-threads",
|
||||
"horizon-world",
|
||||
"log",
|
||||
"miniz_oxide 0.8.9",
|
||||
"naga",
|
||||
"png",
|
||||
"pollster",
|
||||
"rustix 1.1.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1540,11 +1536,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"horizon-core",
|
||||
"horizon-world",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1556,29 +1550,17 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-release-sign"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"ring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"horizon-bench",
|
||||
"horizon-release-sign",
|
||||
"horizon-server-files",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"toml",
|
||||
"toml_edit",
|
||||
"ureq",
|
||||
"windows-sys 0.61.2",
|
||||
"zip",
|
||||
]
|
||||
@@ -1587,7 +1569,6 @@ dependencies = [
|
||||
name = "horizon-server-files"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"horizon-core",
|
||||
"horizon-protocol",
|
||||
"horizon-world",
|
||||
@@ -1620,14 +1601,6 @@ dependencies = [
|
||||
name = "horizon-theme"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "horizon-threads"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-world"
|
||||
version = "0.1.0"
|
||||
@@ -3041,12 +3014,6 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -4621,11 +4588,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "30.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
"ash",
|
||||
"bit-set 0.10.0",
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.6.2",
|
||||
"bytemuck",
|
||||
@@ -4644,13 +4612,11 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-metal 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
"once_cell",
|
||||
"ordered-float",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"profiling",
|
||||
"range-alloc",
|
||||
"raw-window-handle",
|
||||
"raw-window-metal",
|
||||
"renderdoc-sys",
|
||||
@@ -4660,7 +4626,6 @@ dependencies = [
|
||||
"wgpu-naga-bridge",
|
||||
"wgpu-types",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+2
-10
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
exclude = ["launcher", "crates/horizon-telemetry-server", "third_party/wgpu-hal"]
|
||||
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
|
||||
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]
|
||||
@@ -16,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,64 +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="#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** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 JDK, 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, shader packs and data packs:** resource packs work, Java shader packs run their full-screen effects (experimental), and data packs load functions, predicates and item modifiers.
|
||||
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
|
||||
- **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, Advanced Graphics and Shader Packs"></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
|
||||
|
||||
@@ -107,18 +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).
|
||||
|
||||
## 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
|
||||
|
||||
@@ -153,7 +182,18 @@ them too (Rust uses Java's own float maths on each platform).
|
||||
|
||||
### Known issues
|
||||
|
||||
Horizon can't sign in to a Microsoft account yet, so it can't join online-mode servers (it joins offline-mode ones); 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
|
||||
|
||||
|
||||
@@ -29,22 +29,3 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## wgpu-hal
|
||||
|
||||
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
|
||||
developers, with one upstream fix applied
|
||||
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124)). It is licensed under MIT or
|
||||
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
|
||||
## Pixelify Sans
|
||||
|
||||
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
|
||||
by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1.1
|
||||
(`crates/horizon-theme/fonts/OFL-PixelifySans.txt`).
|
||||
|
||||
## Pixelarticons
|
||||
|
||||
`crates/horizon-theme/icons/pixel/*.svg` are icons from
|
||||
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
|
||||
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`).
|
||||
@@ -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 (`packs/fabric.lock.json`) with Lithium
|
||||
`mc26.3-0.26.2-fabric` (`packs/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
|
||||
Same Java and heap.
|
||||
|
||||
The Paper and Fabric jars are dev-only downloads into `.local/server-bench/jars` and are never committed or
|
||||
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)"
|
||||
}
|
||||
@@ -82,19 +82,6 @@ impl CompiledChoices {
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
/// 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<&Variant> {
|
||||
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. A
|
||||
/// multipart root consumes nextLong even for an empty selected subset, then
|
||||
/// resets the same random source to that seed before each selected part.
|
||||
|
||||
@@ -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 }
|
||||
@@ -16,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")]
|
||||
@@ -47,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)]
|
||||
@@ -64,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.
|
||||
@@ -132,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).
|
||||
|
||||
+12
-120
@@ -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>>,
|
||||
@@ -291,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,
|
||||
@@ -330,9 +277,6 @@ fn read(
|
||||
use ids::play::clientbound as cb;
|
||||
use ids::play::serverbound as sb;
|
||||
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body);
|
||||
let mut seen = HashSet::new();
|
||||
let mut batch = BatchSize::new();
|
||||
let mut teleports = 0u64;
|
||||
loop {
|
||||
let (id, body) = match reader.recv() {
|
||||
Ok(packet) => packet,
|
||||
@@ -355,38 +299,14 @@ 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 !bot.held.lock().unwrap().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)
|
||||
&& bot.held.lock().unwrap().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 = bot.teleport.lock().unwrap();
|
||||
@@ -400,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,
|
||||
@@ -500,10 +416,7 @@ fn run_bot(
|
||||
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 !bot.held.lock().unwrap().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 {
|
||||
@@ -527,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,9 +520,6 @@ 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| b.error.lock().unwrap().is_some())
|
||||
@@ -628,11 +536,6 @@ 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;
|
||||
@@ -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": *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}");
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ description = "Horizon native development client"
|
||||
[dependencies]
|
||||
horizon-assets = { path = "../horizon-assets" }
|
||||
horizon-core = { path = "../horizon-core" }
|
||||
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
|
||||
horizon-threads = { path = "../horizon-threads" }
|
||||
horizon-world = { path = "../horizon-world" }
|
||||
horizon-protocol = { path = "../horizon-protocol" }
|
||||
# server.properties, eula.txt and the user lists, shared with horizon-server.
|
||||
@@ -33,55 +31,33 @@ toml = "1.1"
|
||||
naga = { version = "30.0.1", features = ["wgsl-in"] }
|
||||
# Java (OptiFine/Iris) shader packs, translated to WGSL.
|
||||
horizon-shaderpack = { path = "../horizon-shaderpack" }
|
||||
# Sound (ALSA on Linux); the `audio` feature.
|
||||
cpal = { version = "0.16", optional = true }
|
||||
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"] }
|
||||
|
||||
[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(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"]
|
||||
# Records the call sites of mob random draws (`mob_ai_probe` traces).
|
||||
rng-trace = ["horizon-core/rng-trace"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# Every WGSL shader translated as wgpu's DirectX 12 backend does (gfx.rs tests).
|
||||
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
|
||||
@@ -58,12 +58,11 @@ fn main() -> Result<()> {
|
||||
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" {
|
||||
|
||||
@@ -31,14 +31,13 @@ fn main() -> Result<()> {
|
||||
};
|
||||
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)?);
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
//! The Rust side of tools/test_block_entity_tickers_parity.py: the ops of
|
||||
//! `VanillaBlockEntityTickerOracle` on `BlockEntities`, as `World` drives it
|
||||
//! (a state write calls `state_changed`), printing the tick order.
|
||||
use horizon_client::{
|
||||
block_entities::{BlockEntities, Pos, Ticking},
|
||||
containers::BlockEntity,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// State ids of the fixture: 0 air, 1 a block whose block entity ticks, 2 a
|
||||
/// jukebox without a record, 3 a block entity that never ticks (a chest).
|
||||
fn state(name: &str) -> u32 {
|
||||
match name {
|
||||
"chest" | "barrel" => 3,
|
||||
"jukebox" => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn entity(name: &str) -> BlockEntity {
|
||||
if name.starts_with("jukebox") {
|
||||
// The game makes a jukebox's block entity when a record goes in.
|
||||
return BlockEntity::Jukebox(Default::default());
|
||||
}
|
||||
BlockEntity::for_block(&format!("minecraft:{name}"))
|
||||
.unwrap_or_else(|| panic!("no block entity for {name}"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1]).expect("input");
|
||||
let mut out = String::new();
|
||||
let mut entities = BlockEntities::default();
|
||||
let mut states: HashMap<Pos, u32> = HashMap::new();
|
||||
let mut kinds: HashMap<Pos, String> = HashMap::new();
|
||||
let table = vec![Ticking::No, Ticking::Yes, Ticking::Idle, Ticking::No];
|
||||
for line in input.lines() {
|
||||
let w: Vec<&str> = line.split_whitespace().collect();
|
||||
let pos = |i: usize| -> Pos {
|
||||
let n = |j: usize| w[i + j].parse::<i32>().expect("coordinate");
|
||||
(n(0), n(1), n(2))
|
||||
};
|
||||
match w[0] {
|
||||
"reset" => {
|
||||
entities = BlockEntities::default();
|
||||
entities.set_states(table.clone());
|
||||
states.clear();
|
||||
kinds.clear();
|
||||
}
|
||||
"load" => {
|
||||
let mut chunk = Vec::new();
|
||||
for i in (3..w.len()).step_by(4) {
|
||||
let p = pos(i + 1);
|
||||
states.insert(p, state(w[i]));
|
||||
kinds.insert(p, w[i].to_owned());
|
||||
chunk.push((p, entity(w[i])));
|
||||
}
|
||||
entities.load_chunk(chunk);
|
||||
}
|
||||
"place" => {
|
||||
let p = pos(2);
|
||||
// World::set, then the game adds the block entity.
|
||||
let old = states.insert(p, state(w[1])).unwrap_or(0);
|
||||
kinds.insert(p, w[1].to_owned());
|
||||
entities.state_changed(p, old, state(w[1]));
|
||||
entities.insert(p, entity(w[1]));
|
||||
}
|
||||
"replace" => {
|
||||
let p = pos(1);
|
||||
entities.insert(p, entity(&kinds[&p]));
|
||||
}
|
||||
"remove" => {
|
||||
let p = pos(1);
|
||||
entities.remove(&p);
|
||||
let old = states.insert(p, 0).unwrap_or(0);
|
||||
entities.state_changed(p, old, 0);
|
||||
}
|
||||
"record" => {
|
||||
let p = pos(1);
|
||||
let s = if w[4] == "1" { 1 } else { 2 };
|
||||
let old = states.insert(p, s).unwrap_or(0);
|
||||
entities.state_changed(p, old, s);
|
||||
}
|
||||
"unload" => {
|
||||
let c = (w[1].parse::<i32>().unwrap(), w[2].parse::<i32>().unwrap());
|
||||
entities.retain(|p, _| (p.0 >> 4, p.2 >> 4) != c);
|
||||
}
|
||||
"tick" => {
|
||||
out.push_str("tick");
|
||||
for (p, _) in entities.tickers(|p| states.get(&p).copied().unwrap_or(0)) {
|
||||
out.push_str(&format!(" {},{},{}", p.0, p.1, p.2));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
other => panic!("unknown op {other}"),
|
||||
}
|
||||
}
|
||||
std::fs::write(&args[2], out).expect("output");
|
||||
}
|
||||
@@ -12,11 +12,9 @@ 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)?,
|
||||
)?;
|
||||
|
||||
@@ -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];
|
||||
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 @@ 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 @@ 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 @@ 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]]
|
||||
})
|
||||
|
||||
@@ -82,19 +82,12 @@ 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() {
|
||||
|
||||
@@ -31,12 +31,10 @@ fn main() -> Result<()> {
|
||||
};
|
||||
let (seed, size) = (flag("--seed", 12345), flag("--size", 8) as i32);
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Biomes::load(&root)?;
|
||||
let tables = MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(&root, assets.registry)?,
|
||||
)?;
|
||||
@@ -134,7 +132,6 @@ fn main() -> Result<()> {
|
||||
let mut mesher = Mesher {
|
||||
nether_shading: false,
|
||||
assets: &assets,
|
||||
render: &render,
|
||||
tables: &tables,
|
||||
biomes: &biomes,
|
||||
visibility: tables.visibility.clone(),
|
||||
|
||||
@@ -135,7 +135,7 @@ fn tree(d: &Dispatcher, node: usize) -> Value {
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.get(1).map(String::as_str) == Some("tree") {
|
||||
let d = &vanilla::registered(false);
|
||||
let d = vanilla::release_dispatcher();
|
||||
let mut v = tree(d, 0);
|
||||
v["type"] = json!("root");
|
||||
fs::write(&args[2], v.to_string())?;
|
||||
|
||||
@@ -30,11 +30,7 @@ fn main() -> horizon_client::Result<()> {
|
||||
),
|
||||
};
|
||||
let info = biomes.get(biomes.id(&format!("minecraft:{}", f[4])));
|
||||
let mut sky = environment.sample(
|
||||
f[1].parse()?,
|
||||
info.sky.map(horizon_client::biome::rgb),
|
||||
info.fog.map(horizon_client::biome::rgb),
|
||||
);
|
||||
let mut sky = environment.sample(f[1].parse()?, info.sky, info.fog);
|
||||
if dimension == "overworld" {
|
||||
apply_weather(&mut sky, f[2].parse()?, f[3].parse()?);
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Firework tooltip companion for tools/test_firework_tooltip_parity.py: the
|
||||
//! lines `item_tooltip::firework_lines` gives each stack (SNBT, one per line;
|
||||
//! the `#resources` line names the prepared resources for en_us).
|
||||
use horizon_client::{enchant::Enchantments, item_data::parse_stack, item_tooltip::firework_lines};
|
||||
use std::{error::Error, fmt::Write, path::PathBuf};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1])?;
|
||||
let root = input
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix("#resources "))
|
||||
.map(PathBuf::from)
|
||||
.ok_or("no #resources line")?;
|
||||
// Use the inventory's filtered translations so missing tooltip keys are caught.
|
||||
let enchantments = Enchantments::load(&root);
|
||||
let translate = |key: &str| enchantments.translate(key).map(str::to_owned);
|
||||
let mut output = String::new();
|
||||
for line in input.lines().filter(|l| !l.starts_with('#')) {
|
||||
let stack = parse_stack(line)?.ok_or("empty stack")?;
|
||||
let lines = firework_lines(&stack, &translate);
|
||||
if lines.is_empty() {
|
||||
writeln!(output, "-")?;
|
||||
} else {
|
||||
writeln!(output, "{}", lines.join(" | "))?;
|
||||
}
|
||||
}
|
||||
std::fs::write(&args[2], output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -233,32 +233,22 @@ fn mouse(c: &Value, out: &mut impl Write) -> Result<()> {
|
||||
fn scroll(c: &Value, out: &mut impl Write) -> Result<()> {
|
||||
let mut handler = MouseLook::default();
|
||||
let mut slot = c["slot"].as_i64().unwrap() as i32;
|
||||
let spectator = c["spectator"].as_bool().unwrap_or(false);
|
||||
let mut speed = 0.05;
|
||||
// OptionInstance.set quantises the wheel sensitivity.
|
||||
let mut file = OptionsFile::default();
|
||||
file.set("mouseWheelSensitivity", V::Double(db(&c["sens"])));
|
||||
let sensitivity = file.double("mouseWheelSensitivity");
|
||||
writeln!(out, "scroll")?;
|
||||
for op in c["ops"].as_array().unwrap() {
|
||||
if let Some((wheel, (_, y))) =
|
||||
if let Some((wheel, _)) =
|
||||
handler.on_scroll(db(&op[0]), db(&op[1]), b(&c["discrete"]), sensitivity)
|
||||
{
|
||||
if spectator {
|
||||
speed = horizon_client::mouse::spectator_flying_speed(speed, y);
|
||||
} else {
|
||||
slot = horizon_client::mouse::ScrollWheelHandler::next_selection(
|
||||
f64::from(wheel),
|
||||
slot,
|
||||
9,
|
||||
);
|
||||
}
|
||||
}
|
||||
if spectator {
|
||||
writeln!(out, "{slot} {}", fb(speed))?;
|
||||
} else {
|
||||
writeln!(out, "{slot}")?;
|
||||
slot = horizon_client::mouse::ScrollWheelHandler::next_selection(
|
||||
f64::from(wheel),
|
||||
slot,
|
||||
9,
|
||||
);
|
||||
}
|
||||
writeln!(out, "{slot}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//! Selected-slot drop state for the vanilla key-handler parity harness.
|
||||
use horizon_client::command::args::GameMode;
|
||||
use horizon_client::items::{ItemStack, take_held_drop};
|
||||
|
||||
fn main() {
|
||||
for mode in [
|
||||
GameMode::Survival,
|
||||
GameMode::Creative,
|
||||
GameMode::Adventure,
|
||||
GameMode::Spectator,
|
||||
] {
|
||||
for all in [false, true] {
|
||||
for _selected in [0, 8] {
|
||||
for count in [0, 1, 4, 64] {
|
||||
let mut slot = Some(ItemStack::new("minecraft:stone", count));
|
||||
let mut dropped = 0;
|
||||
let mut swings = 0;
|
||||
let mut packets = 0;
|
||||
for _ in 0..2 {
|
||||
if let Some(stack) =
|
||||
take_held_drop(&mut slot, all, mode == GameMode::Spectator)
|
||||
{
|
||||
dropped += stack.count;
|
||||
swings += 1;
|
||||
}
|
||||
if mode != GameMode::Spectator {
|
||||
packets += 1;
|
||||
}
|
||||
println!(
|
||||
"{} {dropped} {swings} {packets}",
|
||||
slot.as_ref().map_or(0, |s| s.count),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use horizon_client::interaction_input::{
|
||||
Button, InteractionInput, InteractionWorld, TargetUseWorld, use_on_target,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
struct World {
|
||||
using: bool,
|
||||
destroying: bool,
|
||||
entity: bool,
|
||||
dig_on_attack: bool,
|
||||
begin_using: bool,
|
||||
delay: u32,
|
||||
events: Vec<String>,
|
||||
passing: bool,
|
||||
}
|
||||
impl InteractionWorld for World {
|
||||
fn using_item(&self) -> bool {
|
||||
self.using
|
||||
}
|
||||
fn release_using(&mut self) {
|
||||
self.events.push("release".into());
|
||||
self.using = false;
|
||||
}
|
||||
fn start_attack(&mut self) -> bool {
|
||||
self.events.push("attack".into());
|
||||
if !self.entity {
|
||||
self.destroying = self.dig_on_attack;
|
||||
}
|
||||
false
|
||||
}
|
||||
fn start_use(&mut self) {
|
||||
if self.destroying {
|
||||
return;
|
||||
}
|
||||
self.delay = 4;
|
||||
let entity = self.entity;
|
||||
use_on_target(self, entity);
|
||||
}
|
||||
fn pick(&mut self) {
|
||||
self.events.push("pick".into());
|
||||
}
|
||||
fn use_delay(&self) -> u32 {
|
||||
self.delay
|
||||
}
|
||||
fn continue_attack(&mut self, down: bool) {
|
||||
if self.using {
|
||||
return;
|
||||
}
|
||||
let down = down && !self.entity;
|
||||
self.events.push(format!("continue {down}"));
|
||||
if !down {
|
||||
self.destroying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TargetUseWorld for World {
|
||||
fn use_entity(&mut self) -> bool {
|
||||
self.events
|
||||
.push(if self.passing { "entity" } else { "use" }.into());
|
||||
if self.passing {
|
||||
false
|
||||
} else {
|
||||
self.using = self.begin_using;
|
||||
true
|
||||
}
|
||||
}
|
||||
fn use_block(&mut self) -> bool {
|
||||
self.events
|
||||
.push(if self.passing { "block" } else { "use" }.into());
|
||||
if self.passing {
|
||||
false
|
||||
} else {
|
||||
self.using = self.begin_using;
|
||||
true
|
||||
}
|
||||
}
|
||||
fn use_item(&mut self) {
|
||||
self.events.push("item".into());
|
||||
self.using = self.begin_using;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
|
||||
for (i, line) in input.lines().enumerate() {
|
||||
let row: Value = serde_json::from_str(&line?)?;
|
||||
let flag = |key: &str| row[key].as_bool().unwrap();
|
||||
let mut world = World {
|
||||
using: flag("using"),
|
||||
destroying: flag("destroying"),
|
||||
entity: flag("entity"),
|
||||
dig_on_attack: flag("dig_on_attack"),
|
||||
begin_using: flag("begin_using"),
|
||||
delay: row["delay"].as_u64().unwrap() as u32,
|
||||
events: vec![],
|
||||
passing: row["passing"].as_bool().unwrap_or(false),
|
||||
};
|
||||
let mut input = InteractionInput::default();
|
||||
for button in row["clicks"].as_array().unwrap() {
|
||||
input.press(match button.as_u64().unwrap() {
|
||||
0 => Button::Attack,
|
||||
1 => Button::Use,
|
||||
_ => Button::Pick,
|
||||
});
|
||||
}
|
||||
input.tick(&mut world, flag("attack_down"), flag("use_down"));
|
||||
writeln!(out, "== {i}")?;
|
||||
for event in world.events {
|
||||
writeln!(out, "{event}")?;
|
||||
}
|
||||
writeln!(
|
||||
out,
|
||||
"state {} {} {}",
|
||||
world.using, world.destroying, world.delay
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,7 +22,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let port: u16 = args.next().ok_or("port")?.parse()?;
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let state = |name: &str| assets.registry.default_state(name).map_or(0, |s| s.0);
|
||||
let state = |name: &str| {
|
||||
assets
|
||||
.registry
|
||||
.default_state(name)
|
||||
.map_or(0, |s| s.0 as u16)
|
||||
};
|
||||
let sync = Arc::new(SyncData::load(&root)?);
|
||||
let biomes: Vec<String> = sync
|
||||
.dynamic
|
||||
@@ -101,9 +106,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
sprinting: false,
|
||||
equipment: Default::default(),
|
||||
health: 20.0,
|
||||
attributes: Vec::new(),
|
||||
bed: None,
|
||||
living_flags: 0,
|
||||
};
|
||||
loop {
|
||||
world.game_time += 1;
|
||||
@@ -119,8 +121,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
clock_rate: 1.0,
|
||||
tick_rate: 20.0,
|
||||
tick_frozen: false,
|
||||
tick_steps: (0, 0),
|
||||
collision: None,
|
||||
};
|
||||
for event in hub.tick(&mut ctx) {
|
||||
match event {
|
||||
@@ -132,9 +132,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
eprintln!("[lan] {packet:?}");
|
||||
}
|
||||
}
|
||||
HostEvent::Chat { line, .. } | HostEvent::Warn(line) | HostEvent::Error(line) => {
|
||||
println!("{line}")
|
||||
}
|
||||
HostEvent::Chat(line) => println!("{line}"),
|
||||
HostEvent::Left(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ fn incremental(
|
||||
sky_light: bool,
|
||||
edits: usize,
|
||||
) {
|
||||
use horizon_client::light::{LightChanges, relight};
|
||||
use horizon_client::light::relight;
|
||||
use std::sync::Arc;
|
||||
let mut world: HashMap<(i32, i32), Arc<Chunk>> = HashMap::new();
|
||||
let inner: Vec<(i32, i32)> = {
|
||||
@@ -232,7 +232,7 @@ fn incremental(
|
||||
c.lit = true;
|
||||
world.insert((x, z), Arc::new(c));
|
||||
}
|
||||
let palette: Vec<u32> = [
|
||||
let palette: Vec<u16> = [
|
||||
"minecraft:air",
|
||||
"minecraft:stone",
|
||||
"minecraft:torch",
|
||||
@@ -242,7 +242,7 @@ fn incremental(
|
||||
"minecraft:glass",
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|n| registry.default_state(n).map(|s| s.0))
|
||||
.filter_map(|n| registry.default_state(n).map(|s| s.0 as u16))
|
||||
.chain(
|
||||
facts
|
||||
.states
|
||||
@@ -257,7 +257,7 @@ fn incremental(
|
||||
]
|
||||
.contains(&&*f.name)
|
||||
})
|
||||
.map(|(i, _)| i as u32)
|
||||
.map(|(i, _)| i as u16)
|
||||
.step_by(3),
|
||||
)
|
||||
.collect();
|
||||
@@ -285,14 +285,7 @@ fn incremental(
|
||||
if old == state || !tables.differs(old, state) {
|
||||
continue;
|
||||
}
|
||||
relight(
|
||||
&mut world,
|
||||
tables,
|
||||
sky_light,
|
||||
&[(x, y, z)],
|
||||
false,
|
||||
&mut LightChanges::default(),
|
||||
);
|
||||
relight(&mut world, tables, sky_light, &[(x, y, z)], false);
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
let (kx, kz) = (cx + dx, cz + dz);
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
//! Read-only saved chunk benchmark. All timings exclude resource/corpus setup.
|
||||
//!
|
||||
//! load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]
|
||||
//! Add --profile-worker when a sampling collector cannot sample the main thread.
|
||||
//! Reports warm filesystem SaveDir::load (read, inflate, NBT, decode and light),
|
||||
//! plus isolated inflate/NBT and native decode. No files in the save are changed.
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
save::{SaveDir, SaveTables, decode_with},
|
||||
};
|
||||
use horizon_world::{nbt::Limits, region::RegionFile};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{hint::black_box, path::PathBuf, sync::Arc, time::Instant};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if std::env::args().any(|a| a == "--profile-worker") {
|
||||
std::thread::spawn(|| run().map_err(|e| e.to_string()))
|
||||
.join()
|
||||
.expect("benchmark worker")
|
||||
.map_err(Into::into)
|
||||
} else {
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let usage = "load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]";
|
||||
let root = PathBuf::from(args.first().ok_or(usage)?);
|
||||
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
|
||||
let flag = |name: &str, default: usize| -> Result<usize> {
|
||||
Ok(match args.iter().position(|a| a == name) {
|
||||
Some(i) => args.get(i + 1).ok_or(usage)?.parse()?,
|
||||
None => default,
|
||||
})
|
||||
};
|
||||
let rounds = flag("--rounds", 5)?;
|
||||
let limit = flag("--limit", 256)?;
|
||||
if rounds == 0 || limit == 0 {
|
||||
return Err("rounds and limit must be positive".into());
|
||||
}
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Biomes::load(&root)?;
|
||||
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
|
||||
let source = SaveDir::new(dimension.clone());
|
||||
let _ = source.tables.set(tables.clone());
|
||||
let mut paths: Vec<_> = std::fs::read_dir(dimension.join("region"))?
|
||||
.map(|e| e.map(|e| e.path()))
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
paths.retain(|p| p.extension().is_some_and(|e| e == "mca"));
|
||||
paths.sort();
|
||||
let mut corpus = Vec::new();
|
||||
'regions: for path in paths {
|
||||
let mut region = RegionFile::open(path)?;
|
||||
let mut positions: Vec<_> = region.chunks().map(|(pos, _)| pos).collect();
|
||||
positions.sort_by_key(|p| (p.x, p.z));
|
||||
for pos in positions {
|
||||
let record = region.read(pos)?.ok_or("missing record")?;
|
||||
let tag = record.nbt(Limits::default())?;
|
||||
if decode_with(&tag, assets.registry, &biomes, Some(&tables)).is_none() {
|
||||
continue;
|
||||
}
|
||||
corpus.push((pos, record, tag));
|
||||
if corpus.len() == limit {
|
||||
break 'regions;
|
||||
}
|
||||
}
|
||||
}
|
||||
if corpus.is_empty() {
|
||||
return Err("no full chunks in corpus".into());
|
||||
}
|
||||
let mut records_hash = Sha256::new();
|
||||
for (pos, record, _) in &corpus {
|
||||
records_hash.update(pos.x.to_le_bytes());
|
||||
records_hash.update(pos.z.to_le_bytes());
|
||||
records_hash.update((record.compressed.len() as u64).to_le_bytes());
|
||||
records_hash.update(&record.compressed);
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"corpus": dimension, "chunks": corpus.len(), "rounds": rounds,
|
||||
"compressed_records_sha256": format!("{:x}", records_hash.finalize()),
|
||||
"compressed_bytes": corpus.iter().map(|(_, r, _)| r.compressed.len()).sum::<usize>()})
|
||||
);
|
||||
// Prime SaveDir's region handles and the filesystem cache before measurement.
|
||||
for (pos, _, _) in &corpus {
|
||||
black_box(
|
||||
source
|
||||
.load(pos.x, pos.z, assets.registry, &biomes)
|
||||
.ok_or("load failed")?,
|
||||
);
|
||||
}
|
||||
for round in 0..rounds {
|
||||
let started = Instant::now();
|
||||
for (pos, _, _) in &corpus {
|
||||
black_box(
|
||||
source
|
||||
.load(pos.x, pos.z, assets.registry, &biomes)
|
||||
.ok_or("load failed")?,
|
||||
);
|
||||
}
|
||||
let load_s = started.elapsed().as_secs_f64();
|
||||
let started = Instant::now();
|
||||
for (_, record, _) in &corpus {
|
||||
black_box(record.nbt(Limits::default())?);
|
||||
}
|
||||
let nbt_s = started.elapsed().as_secs_f64();
|
||||
let started = Instant::now();
|
||||
for (_, _, tag) in &corpus {
|
||||
black_box(
|
||||
decode_with(tag, assets.registry, &biomes, Some(&tables)).ok_or("decode failed")?,
|
||||
);
|
||||
}
|
||||
let decode_s = started.elapsed().as_secs_f64();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"round": round, "load_s": load_s,
|
||||
"inflate_nbt_s": nbt_s, "decode_s": decode_s})
|
||||
);
|
||||
}
|
||||
if args.iter().any(|a| a == "--checksum") {
|
||||
let mut hash = Sha256::new();
|
||||
let mut light_loaded = 0;
|
||||
for (pos, _, _) in &corpus {
|
||||
let chunk = source
|
||||
.load(pos.x, pos.z, assets.registry, &biomes)
|
||||
.ok_or("load failed")?;
|
||||
hash.update(pos.x.to_le_bytes());
|
||||
hash.update(pos.z.to_le_bytes());
|
||||
light_loaded += usize::from(chunk.light_loaded);
|
||||
for section in &chunk.sections {
|
||||
for i in 0..4096 {
|
||||
hash.update(section.get(i).to_le_bytes());
|
||||
hash.update([section.light(i)]);
|
||||
}
|
||||
for i in 0..64 {
|
||||
hash.update(section.biomes.get(i).to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"blocks_biomes_light_sha256": format!("{:x}", hash.finalize()),
|
||||
"light_loaded": light_loaded})
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//! Probe for tools/test_marker_particles_parity.py.
|
||||
use horizon_client::{
|
||||
Result,
|
||||
animate_tick::{AnimateContext, AnimateTables, animate_tick, marker_particle_target},
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
chunk::Chunk,
|
||||
collisions,
|
||||
command::args::GameMode,
|
||||
items::ItemStack,
|
||||
particles::{ParticleEngine, ParticleOptions, ParticleType},
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::random::RandomSource;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let root = Path::new(".local/horizon-client/26.3");
|
||||
let assets = Assets::load(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let biomes = Biomes::load(root)?;
|
||||
let collision = collisions::load(root, assets.registry)?;
|
||||
let tables = AnimateTables::load(root, assets.registry, &biomes)?;
|
||||
let mut world = World::new(facts.air, 0);
|
||||
let barrier = assets
|
||||
.registry
|
||||
.default_state("minecraft:barrier")
|
||||
.ok_or("barrier")?;
|
||||
for cz in -2..2 {
|
||||
for cx in -2..2 {
|
||||
world
|
||||
.chunks
|
||||
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
|
||||
for y in 33..96 {
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
world.set(cx * 16 + x, y, cz * 16 + z, barrier)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut engine = ParticleEngine::new(root, &HashMap::new());
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for line in BufReader::new(File::open(&args[1])?).lines() {
|
||||
let case: Value = serde_json::from_str(&line?)?;
|
||||
let mode = GameMode::ALL
|
||||
.into_iter()
|
||||
.find(|mode| Some(mode.name()) == case["mode"].as_str())
|
||||
.ok_or("mode")?;
|
||||
let stack = ItemStack::new(
|
||||
&format!("minecraft:{}", case["main"].as_str().ok_or("main")?),
|
||||
case["count"].as_u64().ok_or("count")? as u32,
|
||||
);
|
||||
let target = marker_particle_target(mode, Some(&stack));
|
||||
writeln!(out, "{}", target.unwrap_or("none"))?;
|
||||
// Exercise the real ambient sampling loop in a dry barrier-filled world.
|
||||
let options =
|
||||
ParticleOptions::block(ParticleType::BlockMarker, "item/barrier".into(), 0xFFFFFF);
|
||||
let options = |_| Some(options.clone());
|
||||
let ctx = AnimateContext {
|
||||
world: &world,
|
||||
facts: &facts,
|
||||
collision: &collision,
|
||||
biomes: &biomes,
|
||||
registry: assets.registry,
|
||||
raining_at: &|_| false,
|
||||
marker: target.map(|name| (name, &options as &dyn Fn(_) -> _)),
|
||||
};
|
||||
engine.seed_randoms(1057, 0, 0);
|
||||
engine.requested = Some(Vec::new());
|
||||
animate_tick(&tables, &mut engine, &ctx, [0, 64, 0]);
|
||||
for (options, p, v) in engine.requested.take().ok_or("requests")? {
|
||||
assert_eq!(options.ty, ParticleType::BlockMarker);
|
||||
assert_eq!(v, [0.0; 3]);
|
||||
writeln!(out, "marker {} {} {}", p[0], p[1], p[2])?;
|
||||
}
|
||||
writeln!(out, "next {}", engine.random().next_int())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -11,67 +11,20 @@ use horizon_client::{
|
||||
items::Items,
|
||||
menu::{Env, Input, Kind, Menu},
|
||||
};
|
||||
use horizon_protocol::registry::Ids;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// The case's maps: (locked, scale) by id, and the next free id.
|
||||
#[derive(Default)]
|
||||
struct CaseMaps {
|
||||
maps: RefCell<HashMap<i32, (bool, u8)>>,
|
||||
next: Cell<i32>,
|
||||
}
|
||||
|
||||
impl horizon_client::menu::Maps for CaseMaps {
|
||||
fn state(&self, id: i32) -> Option<(bool, u8)> {
|
||||
self.maps.borrow().get(&id).copied()
|
||||
}
|
||||
|
||||
/// `MapItemSavedData.scaled` (unlocked, one scale up) or `locked`.
|
||||
fn derive(&self, id: i32, scale: bool) -> Option<i32> {
|
||||
let (_, s) = self.state(id)?;
|
||||
let new = self.next.get();
|
||||
self.next.set(new + 1);
|
||||
let made = if scale {
|
||||
(false, (s + 1).min(4))
|
||||
} else {
|
||||
(true, s)
|
||||
};
|
||||
self.maps.borrow_mut().insert(new, made);
|
||||
Some(new)
|
||||
}
|
||||
}
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn state(menu: &Menu) -> String {
|
||||
let slots: Vec<String> = (0..menu.slot_count())
|
||||
.map(|i| canon_stack(menu.slot(i)))
|
||||
.collect();
|
||||
let data: String = menu.data.iter().map(|d| format!(" {d}")).collect();
|
||||
let equipped: Vec<String> = menu
|
||||
.equipped
|
||||
.iter()
|
||||
.map(|(slot, stack)| format!("{slot}:{}", canon_stack(stack.as_ref())))
|
||||
.collect();
|
||||
let line = format!(
|
||||
"{} | {} |{data} | {} | {}",
|
||||
format!(
|
||||
"{} | {} |{data} | {}",
|
||||
slots.join(" "),
|
||||
canon_stack(menu.carried.as_ref()),
|
||||
menu.drops.join(";"),
|
||||
equipped.join(";")
|
||||
);
|
||||
if menu.kind() != Kind::Lectern {
|
||||
return line;
|
||||
}
|
||||
// No inventory slots: the inventory Take Book fills.
|
||||
let items: String = (0..36)
|
||||
.map(|i| format!(" {}", canon_stack(menu.player_item(i))))
|
||||
.collect();
|
||||
format!("{line} | inv{items}")
|
||||
menu.drops.join(";")
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
@@ -85,28 +38,13 @@ fn main() -> horizon_client::Result<()> {
|
||||
let recipes = Recipes::load(root, &items)?;
|
||||
let brewing = horizon_client::brewing::Brewing::load(root, &items);
|
||||
let banners = horizon_client::banner::Banners::load(root);
|
||||
let station_recipes = horizon_client::stations::StationRecipes::load(root, &items);
|
||||
let enchantments = horizon_client::enchant::Enchantments::load(root);
|
||||
let names = horizon_client::creative::Names::load(root);
|
||||
let case_maps = CaseMaps::default();
|
||||
let env = Env {
|
||||
items: &items,
|
||||
recipes: &recipes,
|
||||
brewing: &brewing,
|
||||
banners: &banners,
|
||||
station_recipes: &station_recipes,
|
||||
enchantments: &enchantments,
|
||||
names: &names,
|
||||
trade: None,
|
||||
maps: Some(&case_maps),
|
||||
};
|
||||
// The mob effects' network ids (BeaconMenu.encodeEffect).
|
||||
let builtin = horizon_protocol::registry::Builtin::find(root)
|
||||
.and_then(|p| horizon_protocol::registry::Builtin::load(&p).ok());
|
||||
let effects = builtin
|
||||
.as_ref()
|
||||
.and_then(|b| b.get("minecraft:mob_effect"))
|
||||
.cloned();
|
||||
let mut out = String::new();
|
||||
for line in input.lines() {
|
||||
let case: Value = serde_json::from_str(line)?;
|
||||
@@ -124,41 +62,15 @@ fn main() -> horizon_client::Result<()> {
|
||||
.unwrap_or(name),
|
||||
)
|
||||
.ok_or("menu kind")?;
|
||||
// [id, scale, locked] each; the level's new maps from 1000.
|
||||
case_maps.next.set(1000);
|
||||
*case_maps.maps.borrow_mut() = case["maps"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|m| {
|
||||
let id = m[0].as_i64().unwrap_or(0) as i32;
|
||||
(
|
||||
id,
|
||||
(
|
||||
m[2].as_bool().unwrap_or(false),
|
||||
m[1].as_u64().unwrap_or(0) as u8,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut menu = Menu::new(kind);
|
||||
menu.creative = case["creative"].as_bool().unwrap_or(false);
|
||||
menu.levels = case["levels"].as_i64().unwrap_or(0) as i32;
|
||||
for (i, slot) in case["slots"].as_array().ok_or("slots")?.iter().enumerate() {
|
||||
menu.set_slot(&env, i, parse_stack(slot.as_str().unwrap_or("-"))?);
|
||||
}
|
||||
menu.carried = parse_stack(case["carried"].as_str().unwrap_or("-"))?;
|
||||
for (i, item) in case["inventory"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
{
|
||||
menu.set_player_item(i, parse_stack(item.as_str().unwrap_or("-"))?);
|
||||
}
|
||||
menu.settle(&env);
|
||||
for (i, v) in case["data"].as_array().ok_or("data")?.iter().enumerate() {
|
||||
menu.set_data(i, v.as_i64().unwrap_or(0) as i32);
|
||||
menu.data[i] = v.as_i64().unwrap_or(0) as i32;
|
||||
}
|
||||
let mut furnace = furnace_kind.map(|kind| Furnace {
|
||||
kind,
|
||||
@@ -170,7 +82,6 @@ fn main() -> horizon_client::Result<()> {
|
||||
speed: 1.0,
|
||||
});
|
||||
menu.drops.clear();
|
||||
menu.equipped.clear();
|
||||
out += &format!("init {}\n", state(&menu));
|
||||
for op in case["ops"].as_array().ok_or("ops")? {
|
||||
let slot = op[0].as_i64().unwrap_or(0) as i32;
|
||||
@@ -182,26 +93,6 @@ fn main() -> horizon_client::Result<()> {
|
||||
out += &format!("{}\n", state(&menu));
|
||||
continue;
|
||||
}
|
||||
// ServerboundRenameItemPacket: AnvilMenu.setItemName.
|
||||
"RENAME" => {
|
||||
menu.rename(&env, op[3].as_str().unwrap_or_default());
|
||||
out += &format!("{}\n", state(&menu));
|
||||
continue;
|
||||
}
|
||||
// ServerboundSetBeaconPacket: BeaconMenu.updateEffects.
|
||||
"SET_BEACON" => {
|
||||
fn power<'a>(ids: Option<&Ids>, v: &'a Value) -> Option<(i32, &'a str)> {
|
||||
let name = v.as_str()?;
|
||||
Some((ids?.id(name)?, name))
|
||||
}
|
||||
let (primary, secondary) = (
|
||||
power(effects.as_ref(), &op[3]),
|
||||
power(effects.as_ref(), &op[4]),
|
||||
);
|
||||
menu.beacon_update(&env, primary, secondary);
|
||||
out += &format!("{}\n", state(&menu));
|
||||
continue;
|
||||
}
|
||||
// ServerboundContainerSlotStateChangedPacket.
|
||||
"SLOT_STATE" => {
|
||||
if (0..9).contains(&slot) {
|
||||
|
||||
@@ -999,7 +999,6 @@ fn main() -> horizon_client::Result<()> {
|
||||
hero: false,
|
||||
uuid: PLAYER_UUID,
|
||||
allies: Default::default(),
|
||||
remote: false,
|
||||
}
|
||||
});
|
||||
let mut scratch = LegacyRandom::new(0);
|
||||
@@ -1024,10 +1023,8 @@ fn main() -> horizon_client::Result<()> {
|
||||
collision: &collision,
|
||||
data: &data,
|
||||
player: player.clone(),
|
||||
nearest_player: player.as_ref().filter(|p| !p.spectator).map(|p| p.position),
|
||||
others: $snapshot,
|
||||
others_grid: $grid,
|
||||
others_index: None,
|
||||
daylight: (world.day_time.rem_euclid(24000)) < 12000,
|
||||
game_time: world.game_time,
|
||||
day_time: world.day_time,
|
||||
@@ -1133,7 +1130,7 @@ fn main() -> horizon_client::Result<()> {
|
||||
}
|
||||
#[allow(unused_mut)] // filled only with the rng-trace feature
|
||||
let mut traces: Vec<String> = Vec::new();
|
||||
let mut pushes = horizon_client::mobs::PendingPushes::default();
|
||||
let mut pushes = Vec::new();
|
||||
let mut born = Vec::new();
|
||||
for i in 0..mobs.len() {
|
||||
if mobs[i].removed {
|
||||
|
||||
@@ -84,10 +84,8 @@ fn main() -> Result<()> {
|
||||
horizon_client::collisions::load(&root, registry).map_err(step("collisions"))?;
|
||||
let loaded = began.elapsed().as_secs_f64() - built;
|
||||
let ours = |id: &str| {
|
||||
id.split_once(':').is_some_and(|(ns, _)| {
|
||||
mods.iter()
|
||||
.any(|m| m.content.namespaces.iter().any(|n| n == ns))
|
||||
})
|
||||
id.split_once(':')
|
||||
.is_some_and(|(ns, _)| mods.iter().any(|m| m.id == ns))
|
||||
};
|
||||
let tools: Vec<String> = serde_json::from_value(oracle["tools"].clone())?;
|
||||
|
||||
|
||||
@@ -56,11 +56,10 @@ fn main() -> horizon_client::Result<()> {
|
||||
let initial = LIVE.load(Relaxed);
|
||||
let start = Instant::now();
|
||||
let assets = Assets::load(root)?;
|
||||
let render = assets.reload(root)?;
|
||||
let block_ms = start.elapsed().as_secs_f64() * 1000.;
|
||||
let block_retained = LIVE.load(Relaxed) - initial;
|
||||
let start_items = Instant::now();
|
||||
let items = ItemRenderer::load_with_library(root, render.model_library.clone())?;
|
||||
let items = ItemRenderer::load_with_library(root, assets.model_library.clone())?;
|
||||
let item_ms = start_items.elapsed().as_secs_f64() * 1000.;
|
||||
let total_ms = start.elapsed().as_secs_f64() * 1000.;
|
||||
let retained = LIVE.load(Relaxed) - initial;
|
||||
@@ -75,7 +74,7 @@ fn main() -> horizon_client::Result<()> {
|
||||
horizon_core::position::BlockPos::new(0, 0, 0),
|
||||
horizon_core::position::BlockPos::new(-17, 65, 31),
|
||||
] {
|
||||
if let Ok(selected) = render.model_catalog.collect(id, pos) {
|
||||
if let Ok(selected) = assets.model_catalog.collect(id, pos) {
|
||||
for (variant, model) in selected {
|
||||
format!("{variant:?}").hash(&mut hash);
|
||||
model.ambient_occlusion.hash(&mut hash);
|
||||
|
||||
@@ -111,8 +111,6 @@ struct World<'a> {
|
||||
slime: f32,
|
||||
starts: Vec<Start>,
|
||||
players: Vec<([f64; 3], String)>,
|
||||
/// Where the non-spectators among them are.
|
||||
nearest: Vec<[f64; 3]>,
|
||||
respawn: Option<[i32; 3]>,
|
||||
unloaded: HashSet<(i32, i32)>,
|
||||
spawn_mobs: bool,
|
||||
@@ -270,16 +268,10 @@ impl NaturalLevel for World<'_> {
|
||||
self.biome_info(x, y, z)
|
||||
.map_or(0.1, |b| b.creature_probability)
|
||||
}
|
||||
fn structures_at(
|
||||
&self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
wanted: &dyn Fn(&str) -> bool,
|
||||
) -> Vec<StructureAt> {
|
||||
fn structures_at(&self, x: i32, y: i32, z: i32) -> Vec<StructureAt> {
|
||||
let (cx, cz) = (x >> 4, z >> 4);
|
||||
let mut out: Vec<StructureAt> = Vec::new();
|
||||
for start in self.starts.iter().filter(|s| wanted(&s.id)) {
|
||||
for start in &self.starts {
|
||||
let b = start.bounds.0;
|
||||
// Referenced: the start's box reaches the chunk's columns.
|
||||
if b[3] < cx * 16 || b[0] > cx * 16 + 15 || b[5] < cz * 16 || b[2] > cz * 16 + 15 {
|
||||
@@ -304,8 +296,19 @@ impl NaturalLevel for World<'_> {
|
||||
fn structure_spawns(&self) -> &StructureSpawns {
|
||||
self.s.structures
|
||||
}
|
||||
fn nearest_player_positions(&self) -> &[[f64; 3]] {
|
||||
&self.nearest
|
||||
fn nearest_player_distance_sqr(&self, x: f64, y: f64, z: f64) -> Option<f64> {
|
||||
let mut best: Option<f64> = None;
|
||||
for (p, mode) in &self.players {
|
||||
if mode == "spectator" {
|
||||
continue;
|
||||
}
|
||||
let d = [p[0] - x, p[1] - y, p[2] - z];
|
||||
let d = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
|
||||
if best.is_none_or(|b| d < b) {
|
||||
best = Some(d);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
fn respawn(&self) -> Option<[i32; 3]> {
|
||||
self.respawn
|
||||
@@ -721,11 +724,6 @@ fn main() -> horizon_client::Result<()> {
|
||||
dimension_id: dimension_id.clone(),
|
||||
slime: get("slime", "0.0").parse()?,
|
||||
starts,
|
||||
nearest: players
|
||||
.iter()
|
||||
.filter(|(_, mode)| mode != "spectator")
|
||||
.map(|(p, _)| *p)
|
||||
.collect(),
|
||||
players,
|
||||
respawn,
|
||||
unloaded,
|
||||
|
||||
@@ -112,26 +112,21 @@ fn state(s: &ParticleSnapshot) -> String {
|
||||
}
|
||||
out
|
||||
}
|
||||
fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Result<World> {
|
||||
fn world(assets: &Assets, facts: &BlockFacts, floor: bool) -> Result<World> {
|
||||
let mut world = World::new(facts.air, 0);
|
||||
let state = |name: &str| assets.registry.default_state(name).ok_or("missing block");
|
||||
let (stone, still) = (state("minecraft:stone")?, state("minecraft:water")?);
|
||||
let stone = assets
|
||||
.registry
|
||||
.default_state("minecraft:stone")
|
||||
.ok_or("missing stone")?;
|
||||
for cz in -3..=2 {
|
||||
for cx in -3..=2 {
|
||||
world
|
||||
.chunks
|
||||
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
let (x, z) = (cx * 16 + x, cz * 16 + z);
|
||||
if floor {
|
||||
world.set(x, 63, z, stone)?;
|
||||
}
|
||||
// The oracle's water: still sources from y 64 to 67.
|
||||
if water {
|
||||
for y in 64..=67 {
|
||||
world.set(x, y, z, still)?;
|
||||
}
|
||||
if floor {
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
world.set(cx * 16 + x, 63, cz * 16 + z, stone)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,15 +137,13 @@ fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Resul
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let root = &Path::new(".local/horizon-client").join(horizon_core::VANILLA_VERSION);
|
||||
let root = Path::new(".local/horizon-client/26.3");
|
||||
let assets = Assets::load(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let collision = collisions::load(root, assets.registry)?;
|
||||
let worlds = [
|
||||
world(&assets, &facts, false, false)?,
|
||||
world(&assets, &facts, true, false)?,
|
||||
world(&assets, &facts, false, true)?,
|
||||
world(&assets, &facts, true, true)?,
|
||||
world(&assets, &facts, false)?,
|
||||
world(&assets, &facts, true)?,
|
||||
];
|
||||
let mut engine = ParticleEngine::new(root, &HashMap::new());
|
||||
for (ty, item) in [
|
||||
@@ -181,8 +174,7 @@ fn main() -> Result<()> {
|
||||
engine.requested = Some(Vec::new());
|
||||
engine.sounds.clear();
|
||||
let level = ParticleLevel {
|
||||
world: &worlds[usize::from(case["floor"].as_bool() == Some(true))
|
||||
+ 2 * usize::from(case["water"].as_bool() == Some(true))],
|
||||
world: &worlds[usize::from(case["floor"].as_bool() == Some(true))],
|
||||
facts: &facts,
|
||||
collision: &collision,
|
||||
player: None,
|
||||
|
||||
@@ -38,19 +38,12 @@ fn main() -> horizon_client::Result<()> {
|
||||
let recipes = Recipes::load(root, &items)?;
|
||||
let brewing = horizon_client::brewing::Brewing::load(root, &items);
|
||||
let banners = horizon_client::banner::Banners::default();
|
||||
let station_recipes = horizon_client::stations::StationRecipes::default();
|
||||
let enchantments = horizon_client::enchant::Enchantments::default();
|
||||
let names = horizon_client::creative::Names::default();
|
||||
let env = Env {
|
||||
items: &items,
|
||||
recipes: &recipes,
|
||||
brewing: &brewing,
|
||||
banners: &banners,
|
||||
station_recipes: &station_recipes,
|
||||
enchantments: &enchantments,
|
||||
names: &names,
|
||||
trade: None,
|
||||
maps: None,
|
||||
};
|
||||
let mut out = String::new();
|
||||
for (line, order) in cases.lines().zip(orders.lines()) {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
use horizon_client::player_sync::{PositionSender, teleport_velocity};
|
||||
use horizon_core::geometry::Vec3;
|
||||
use horizon_protocol::packets::play::{PlayerPosition, Serverbound as S};
|
||||
use serde_json::Value;
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
fn number(value: &Value) -> f64 {
|
||||
f64::from_bits(u64::from_str_radix(value.as_str().unwrap(), 16).unwrap())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
|
||||
for line in input.lines() {
|
||||
let row: Value = serde_json::from_str(&line?)?;
|
||||
if row["kind"] == "movement" {
|
||||
let mut sender = PositionSender::default();
|
||||
for op in row["ops"].as_array().unwrap() {
|
||||
let pos = std::array::from_fn(|i| number(&op[i]));
|
||||
let rot = (number(&op[3]) as f32, number(&op[4]) as f32);
|
||||
if let Some(packet) =
|
||||
sender.update(pos, rot, op[5].as_bool().unwrap(), op[6].as_bool().unwrap())
|
||||
{
|
||||
let name = match packet {
|
||||
S::MovePlayerPos(_) => "pos",
|
||||
S::MovePlayerPosRot(_) => "posrot",
|
||||
S::MovePlayerRot(_) => "rot",
|
||||
S::MovePlayerStatus(_) => "status",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
write!(out, "{name} ")?;
|
||||
for byte in &packet.encode()[1..] {
|
||||
write!(out, "{byte:02x}")?;
|
||||
}
|
||||
writeln!(out)?;
|
||||
} else {
|
||||
writeln!(out, "-")?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let vector = |key: &str| {
|
||||
Vec3::new(
|
||||
number(&row[key][0]),
|
||||
number(&row[key][1]),
|
||||
number(&row[key][2]),
|
||||
)
|
||||
};
|
||||
let rotation = |key: &str| (number(&row[key][0]) as f32, number(&row[key][1]) as f32);
|
||||
let old = rotation("old");
|
||||
let new = rotation("new");
|
||||
let delta = vector("delta");
|
||||
let packet = PlayerPosition {
|
||||
teleport: 0,
|
||||
position: [0.0; 3],
|
||||
delta: [delta.x, delta.y, delta.z],
|
||||
yaw: new.0,
|
||||
pitch: new.1,
|
||||
relatives: row["flags"].as_i64().unwrap() as i32,
|
||||
};
|
||||
let v = teleport_velocity(vector("velocity"), old, new, &packet);
|
||||
writeln!(
|
||||
out,
|
||||
"{:016x} {:016x} {:016x}",
|
||||
v.x.to_bits(),
|
||||
v.y.to_bits(),
|
||||
v.z.to_bits()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
//! The `entity_portals` section scan only: `may_hold_portal` over synthetic
|
||||
//! entity reaches, with the per-tick palette map against the resident cache.
|
||||
//! Run with prepared client resources:
|
||||
//! cargo run --profile fast -p horizon-client --example portal_scan_bench -- RESOURCES
|
||||
use horizon_client::{
|
||||
blocks::BlockFacts,
|
||||
chunk::{Chunk, MIN_Y, SECTIONS, Section},
|
||||
fast_hash::FxHashMap,
|
||||
portal_sections::PortalSections,
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3},
|
||||
position::BlockPos,
|
||||
};
|
||||
use horizon_world::{level::Behaviours, registry::BlockStateId};
|
||||
use std::{hint::black_box, path::Path, sync::Arc, time::Instant};
|
||||
|
||||
/// `Game::swept_portal`'s reach (`Entity.checkInsideBlocks`'s swept box).
|
||||
fn reach(from: Vec3, to: Vec3, width: f32, height: f32) -> Aabb {
|
||||
let half = f64::from(width) / 2.0;
|
||||
let bounds = Aabb::new(
|
||||
Vec3::new(to.x - half, to.y, to.z - half),
|
||||
Vec3::new(to.x + half, to.y + f64::from(height), to.z + half),
|
||||
)
|
||||
.deflate(Vec3::new(1.0e-5, 1.0e-5, 1.0e-5));
|
||||
bounds
|
||||
.union_bounds(bounds.move_by(from.subtract(to)))
|
||||
.inflate(Vec3::new(1.0, 1.0, 1.0))
|
||||
}
|
||||
|
||||
fn sections(reach: Aabb) -> (i64, i32, i32, i32, i32, i32, i32) {
|
||||
let (min, max) = (reach.min_position(), reach.max_position());
|
||||
let (min, max) = (BlockPos::containing(min), BlockPos::containing(max));
|
||||
let top = SECTIONS as i32 - 1;
|
||||
let (x0, x1) = (min.x >> 4, max.x >> 4);
|
||||
let (z0, z1) = (min.z >> 4, max.z >> 4);
|
||||
let (y0, y1) = (
|
||||
((min.y - MIN_Y) >> 4).clamp(0, top),
|
||||
((max.y - MIN_Y) >> 4).clamp(0, top),
|
||||
);
|
||||
let count = (i64::from(x1) - i64::from(x0) + 1)
|
||||
* (i64::from(y1) - i64::from(y0) + 1)
|
||||
* (i64::from(z1) - i64::from(z0) + 1);
|
||||
(count, x0, x1, y0, y1, z0, z1)
|
||||
}
|
||||
|
||||
/// The original `Game::may_hold_portal`: per-tick `seen`, palette per section.
|
||||
fn may_hold_portal_baseline(
|
||||
reach: Aabb,
|
||||
world: &FxHashMap<(i32, i32), Arc<Chunk>>,
|
||||
seen: &mut FxHashMap<(i32, i32, i32), bool>,
|
||||
behaviours: &Behaviours,
|
||||
) -> bool {
|
||||
let (count, x0, x1, y0, y1, z0, z1) = sections(reach);
|
||||
if count > 64 {
|
||||
return true;
|
||||
}
|
||||
(x0..=x1).any(|cx| {
|
||||
(z0..=z1).any(|cz| {
|
||||
(y0..=y1).any(|sy| {
|
||||
*seen.entry((cx, sy, cz)).or_insert_with(|| {
|
||||
world
|
||||
.get(&(cx, cz))
|
||||
.and_then(|c| c.sections.get(sy as usize))
|
||||
.is_some_and(|s| {
|
||||
s.blocks
|
||||
.maybe_has(|v| behaviours.portal_axis(BlockStateId(v)).is_some())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The cached `Game::may_hold_portal`: one mask per resident chunk.
|
||||
fn may_hold_portal_cached(
|
||||
reach: Aabb,
|
||||
world: &FxHashMap<(i32, i32), Arc<Chunk>>,
|
||||
cache: &mut PortalSections,
|
||||
behaviours: &Behaviours,
|
||||
) -> bool {
|
||||
let (count, x0, x1, y0, y1, z0, z1) = sections(reach);
|
||||
if count > 64 {
|
||||
return true;
|
||||
}
|
||||
let mut y_mask = 0u32;
|
||||
for sy in y0..=y1 {
|
||||
y_mask |= 1 << sy;
|
||||
}
|
||||
(x0..=x1).any(|cx| {
|
||||
(z0..=z1).any(|cz| {
|
||||
world.get(&(cx, cz)).is_some_and(|chunk| {
|
||||
cache.get((cx, cz), chunk, |v| {
|
||||
behaviours.portal_axis(BlockStateId(v)).is_some()
|
||||
}) & y_mask
|
||||
!= 0
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let root = std::env::args()
|
||||
.nth(1)
|
||||
.ok_or("expected prepared client resource directory")?;
|
||||
let start = Instant::now();
|
||||
let registry = horizon_world::level::data::load_registry(Path::new(&root))?;
|
||||
let (facts, behaviours) = BlockFacts::load_pair(Path::new(&root), ®istry)?;
|
||||
println!(
|
||||
"resource_load_ms={:.3}",
|
||||
start.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
let len = registry.len() as u32;
|
||||
let portal: Vec<u32> = (0..len)
|
||||
.filter(|&id| behaviours.portal_axis(BlockStateId(id)).is_some())
|
||||
.collect();
|
||||
let inactive_states: Vec<u32> = (0..len)
|
||||
.filter(|&id| behaviours.portal_axis(BlockStateId(id)).is_none())
|
||||
.collect();
|
||||
let _ = &facts;
|
||||
let portal_state = *portal
|
||||
.first()
|
||||
.ok_or("no nether portal block state in the registry")?;
|
||||
println!("portal_states={}", portal.len());
|
||||
|
||||
// 500 entities spread over a 17x17 chunk world. Their reaches touch 1-4
|
||||
// sections, as an item or a mob standing in or stepping into a portal does.
|
||||
let grid = 17;
|
||||
let base = -(grid / 2) * 16;
|
||||
let entities: Vec<(Vec3, Vec3, f32, f32)> = (0..500)
|
||||
.map(|i| {
|
||||
let x = base + (i % grid) * 16 + (i % 5);
|
||||
let z = base + (i / grid % grid) * 16 + (i % 7);
|
||||
let y = 64 + (i % 3) * 16;
|
||||
let from = Vec3::new(f64::from(x), f64::from(y), f64::from(z));
|
||||
let to = from.add_vector(Vec3::new(0.1, 0.0, 0.07));
|
||||
(from, to, 0.25, 0.25)
|
||||
})
|
||||
.collect();
|
||||
let reaches: Vec<Aabb> = entities
|
||||
.iter()
|
||||
.map(|&(from, to, w, h)| reach(from, to, w, h))
|
||||
.collect();
|
||||
|
||||
for (name, palette, portals_every) in [
|
||||
("sparse", 1usize, 0usize),
|
||||
("dense", 128, 0),
|
||||
("global", 300, 0),
|
||||
("dense_portal", 128, 5),
|
||||
] {
|
||||
let start = Instant::now();
|
||||
let mut world: FxHashMap<(i32, i32), Arc<Chunk>> = FxHashMap::default();
|
||||
for czi in 0..grid {
|
||||
for cxi in 0..grid {
|
||||
let (cx, cz) = (-(grid / 2) + cxi, -(grid / 2) + czi);
|
||||
let mut chunk = Chunk::empty(cx, cz);
|
||||
for (i, section) in chunk.sections.iter_mut().enumerate() {
|
||||
let values: [u32; 4096] = std::array::from_fn(|j| {
|
||||
if portals_every > 0 && i % portals_every == 0 && j == 0 {
|
||||
portal_state
|
||||
} else {
|
||||
inactive_states[(j * 7 + i) % palette.min(inactive_states.len())]
|
||||
}
|
||||
});
|
||||
Arc::make_mut(section).blocks = Section::from_blocks(&values);
|
||||
}
|
||||
world.insert((cx, cz), Arc::new(chunk));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{name} fixture_ms={:.3}",
|
||||
start.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
let keys: Vec<_> = world.keys().copied().collect();
|
||||
let portal_chunks: Vec<(i32, i32)> = keys
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&(cx, cz)| {
|
||||
world[&(cx, cz)]
|
||||
.sections
|
||||
.iter()
|
||||
.any(|s| s.blocks.maybe_has(|v| v == portal_state))
|
||||
})
|
||||
.collect();
|
||||
println!("{name} portal_chunks={}", portal_chunks.len());
|
||||
// The global palette's maybe_has walks the whole ID range, so rescanning
|
||||
// every chunk each tick (cold/reload/edit_all) is intractable; those rows
|
||||
// are only meaningful for the palettes whose scans are bounded.
|
||||
let workloads: &[(&str, usize)] = if name == "global" {
|
||||
&[("steady", 0), ("edit_1pct", 3)]
|
||||
} else {
|
||||
&[
|
||||
("steady", 0),
|
||||
("cold", 0),
|
||||
("reload", 0),
|
||||
("edit_1pct", 3),
|
||||
("edit_all", 289),
|
||||
]
|
||||
};
|
||||
for &(workload, changed) in workloads {
|
||||
let mut baseline = Vec::new();
|
||||
let mut candidate = Vec::new();
|
||||
let mut cache = PortalSections::default();
|
||||
let (mut base_hits, mut cand_hits) = (0u64, 0u64);
|
||||
for sample in 0..8 {
|
||||
for cached in if sample % 2 == 0 {
|
||||
[false, true]
|
||||
} else {
|
||||
[true, false]
|
||||
} {
|
||||
let start = Instant::now();
|
||||
let mut result = 0u64;
|
||||
for tick in 0..300 {
|
||||
if cached && workload == "cold" {
|
||||
cache = PortalSections::default();
|
||||
}
|
||||
if cached && workload == "reload" {
|
||||
cache.clear();
|
||||
}
|
||||
for key in keys.iter().take(changed) {
|
||||
Arc::make_mut(world.get_mut(key).unwrap()).set(
|
||||
0,
|
||||
MIN_Y,
|
||||
0,
|
||||
inactive_states[tick % inactive_states.len()],
|
||||
);
|
||||
}
|
||||
let mut seen = FxHashMap::default();
|
||||
for reach in &reaches {
|
||||
let hit = if cached {
|
||||
may_hold_portal_cached(*reach, &world, &mut cache, &behaviours)
|
||||
} else {
|
||||
may_hold_portal_baseline(*reach, &world, &mut seen, &behaviours)
|
||||
};
|
||||
result += hit as u64;
|
||||
}
|
||||
}
|
||||
if cached {
|
||||
cache = PortalSections::default();
|
||||
cand_hits += black_box(result);
|
||||
} else {
|
||||
base_hits += black_box(result);
|
||||
}
|
||||
if sample > 0 {
|
||||
let us = start.elapsed().as_secs_f64() * 1e6 / 300.0;
|
||||
if cached {
|
||||
candidate.push(us);
|
||||
} else {
|
||||
baseline.push(us);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
base_hits, cand_hits,
|
||||
"{name} {workload}: cached may_hold_portal diverged"
|
||||
);
|
||||
println!("{name} {workload} baseline_us={baseline:.3?} candidate_us={candidate:.3?}");
|
||||
}
|
||||
}
|
||||
println!("chunk_wrapper_bytes={}", std::mem::size_of::<Chunk>());
|
||||
Ok(())
|
||||
}
|
||||
@@ -56,16 +56,16 @@ fn main() -> horizon_client::Result<()> {
|
||||
"resource_load_ms={:.3}",
|
||||
start.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
let inactive: Vec<u32> = facts
|
||||
let inactive: Vec<u16> = facts
|
||||
.states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| !f.random_ticks)
|
||||
.take(128)
|
||||
.map(|(i, _)| i as u32)
|
||||
.map(|(i, _)| i as u16)
|
||||
.collect();
|
||||
let active = registry.default_state("minecraft:grass_block").unwrap().0;
|
||||
assert!(facts.get(BlockStateId(active)).random_ticks);
|
||||
let active = registry.default_state("minecraft:grass_block").unwrap().0 as u16;
|
||||
assert!(facts.get(BlockStateId(u32::from(active))).random_ticks);
|
||||
for (name, palette) in [("sparse", 1), ("dense", 128)] {
|
||||
let start = Instant::now();
|
||||
let mut chunks = FxHashMap::default();
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
//! Random-tick sections companion for tools/test_random_tick_sections_parity.py:
|
||||
//! the game's `RandomTickSections` choosing the sections of a chunk that
|
||||
//! `Behaviours::tick_chunk_random` ticks, after the case's edits (see
|
||||
//! VanillaRandomTickSectionsOracle for the format; the `#resources` line names
|
||||
//! the prepared resources). The level the ticks run against reads as air
|
||||
//! everywhere, so nothing ticks for real: its reads are the drawn positions.
|
||||
use horizon_client::{
|
||||
blocks::BlockFacts,
|
||||
chunk::{Chunk, MIN_Y, Section},
|
||||
random_tick_sections::RandomTickSections,
|
||||
};
|
||||
use horizon_core::{
|
||||
position::{BlockPos, ChunkPos},
|
||||
random::LegacyRandom,
|
||||
};
|
||||
use horizon_world::{
|
||||
fluid::FluidKind,
|
||||
level::{GameRules, LevelAccess, LevelEvent, Ticks, data},
|
||||
registry::BlockStateId,
|
||||
};
|
||||
use std::{cell::RefCell, error::Error, fmt::Write, path::PathBuf, sync::Arc};
|
||||
|
||||
struct Air {
|
||||
reads: RefCell<Vec<BlockPos>>,
|
||||
random: LegacyRandom,
|
||||
speed: i32,
|
||||
}
|
||||
impl LevelAccess for Air {
|
||||
fn block_state(&self, pos: BlockPos) -> BlockStateId {
|
||||
self.reads.borrow_mut().push(pos);
|
||||
BlockStateId(0)
|
||||
}
|
||||
fn set_raw(&mut self, _: BlockPos, _: BlockStateId) -> Option<BlockStateId> {
|
||||
None
|
||||
}
|
||||
fn is_loaded(&self, _: BlockPos) -> bool {
|
||||
true
|
||||
}
|
||||
fn random(&mut self) -> &mut LegacyRandom {
|
||||
&mut self.random
|
||||
}
|
||||
fn game_time(&self) -> i64 {
|
||||
0
|
||||
}
|
||||
fn game_rules(&self) -> GameRules {
|
||||
GameRules {
|
||||
random_tick_speed: self.speed,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
fn emit(&mut self, _: LevelEvent) {}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1])?;
|
||||
let root = input
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix("#resources "))
|
||||
.map(PathBuf::from)
|
||||
.ok_or("no #resources line")?;
|
||||
let registry = data::load_registry(&root)?;
|
||||
let (facts, behaviours) = BlockFacts::load_pair(&root, ®istry)?;
|
||||
let mut output = String::new();
|
||||
for line in input.lines().filter(|l| !l.starts_with('#')) {
|
||||
let mut words = line.split(' ');
|
||||
let mut ticks = Ticks::new(0);
|
||||
ticks.random_tick_value = words.next().ok_or("no randValue")?.parse()?;
|
||||
let mut level = Air {
|
||||
reads: RefCell::default(),
|
||||
random: LegacyRandom::new(0),
|
||||
speed: words.next().ok_or("no speed")?.parse()?,
|
||||
};
|
||||
let mut chunk = Arc::new(Chunk::empty(0, 0));
|
||||
let mut cache = RandomTickSections::default();
|
||||
for word in words {
|
||||
let op: Vec<&str> = word.split(':').collect();
|
||||
let int = |i: usize| -> Result<i32, Box<dyn Error>> { Ok(op[i].parse()?) };
|
||||
match op[0] {
|
||||
"s" => {
|
||||
let (section, index) = (int(1)?, int(2)?);
|
||||
Arc::make_mut(&mut chunk).set(
|
||||
index & 15,
|
||||
MIN_Y + section * 16 + (index >> 8),
|
||||
index >> 4 & 15,
|
||||
int(3)? as u32,
|
||||
);
|
||||
}
|
||||
"f" => {
|
||||
let (a, b, m) = (int(2)? as u32, int(3)? as u32, int(4)? as usize);
|
||||
let values = std::array::from_fn(|i| if i % m == m - 1 { b } else { a });
|
||||
Arc::make_mut(&mut Arc::make_mut(&mut chunk).sections[int(1)? as usize])
|
||||
.blocks = Section::from_blocks(&values);
|
||||
}
|
||||
"t" => {
|
||||
output.push('|');
|
||||
level.reads.borrow_mut().clear();
|
||||
behaviours.tick_chunk_random(
|
||||
&mut level,
|
||||
&mut ticks,
|
||||
ChunkPos::new(0, 0),
|
||||
|_, from| cache.get((0, 0), &chunk, &facts).from(from),
|
||||
);
|
||||
for pos in level.reads.borrow().iter() {
|
||||
let state = chunk.block(pos.x, pos.y, pos.z);
|
||||
let fluid = matches!(
|
||||
behaviours.fluid(state).kind(),
|
||||
FluidKind::Lava | FluidKind::FlowingLava
|
||||
);
|
||||
write!(
|
||||
output,
|
||||
" {},{},{},{}{}",
|
||||
pos.x,
|
||||
pos.y,
|
||||
pos.z,
|
||||
if behaviours.is_randomly_ticking(state) {
|
||||
'B'
|
||||
} else {
|
||||
'-'
|
||||
},
|
||||
if fluid { 'F' } else { '-' },
|
||||
)?;
|
||||
}
|
||||
}
|
||||
other => return Err(format!("unknown op {other}").into()),
|
||||
}
|
||||
}
|
||||
writeln!(output, " | {}", ticks.random_tick_value)?;
|
||||
}
|
||||
std::fs::write(&args[2], output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
//! Resident memory of each resource loader, in the game's start order: what a
|
||||
//! larger registry (content mods, docs/modpacks.md) costs, loader by loader.
|
||||
//!
|
||||
//! resource_memory RESOURCES
|
||||
//!
|
||||
//! RESOURCES is a prepared root or a content-mod root (`.horizon-cache/mod-content`).
|
||||
//! Prints, after each step, the live heap (a counting allocator: what the
|
||||
//! step keeps), the heap's peak during the step (what it parsed on the way)
|
||||
//! and the process RSS (what the allocator holds on to). On macOS it ends
|
||||
//! with `footprint`'s physical footprint, the number the OS charges: RSS also
|
||||
//! counts freed pages the allocator marked reusable.
|
||||
use horizon_client::{
|
||||
assets::Assets, biome::Biomes, blocks::BlockFacts, collisions, crafting::Recipes, items::Items,
|
||||
light::LightTables, mesher::MeshTables, models, motion::MotionResources,
|
||||
vanillagen::VanillaTerrain,
|
||||
};
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
sync::atomic::{AtomicUsize, Ordering::Relaxed},
|
||||
};
|
||||
use std::{path::PathBuf, process::Command};
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
struct Counting;
|
||||
static LIVE: AtomicUsize = AtomicUsize::new(0);
|
||||
static PEAK: AtomicUsize = AtomicUsize::new(0);
|
||||
// SAFETY: forwards to the system allocator, only counting sizes.
|
||||
unsafe impl GlobalAlloc for Counting {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
let p = unsafe { System.alloc(layout) };
|
||||
if !p.is_null() {
|
||||
let now = LIVE.fetch_add(layout.size(), Relaxed) + layout.size();
|
||||
PEAK.fetch_max(now, Relaxed);
|
||||
}
|
||||
p
|
||||
}
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe { System.dealloc(ptr, layout) };
|
||||
LIVE.fetch_sub(layout.size(), Relaxed);
|
||||
}
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new: usize) -> *mut u8 {
|
||||
let p = unsafe { System.realloc(ptr, layout, new) };
|
||||
if !p.is_null() {
|
||||
if new >= layout.size() {
|
||||
let now = LIVE.fetch_add(new - layout.size(), Relaxed) + new - layout.size();
|
||||
PEAK.fetch_max(now, Relaxed);
|
||||
} else {
|
||||
LIVE.fetch_sub(layout.size() - new, Relaxed);
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
}
|
||||
#[global_allocator]
|
||||
static ALLOC: Counting = Counting;
|
||||
const MB: f64 = 1048576.0;
|
||||
|
||||
fn rss_mb() -> f64 {
|
||||
let out = Command::new("ps")
|
||||
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
|
||||
.output()
|
||||
.ok();
|
||||
out.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.and_then(|s| s.trim().parse::<f64>().ok())
|
||||
.map_or(0.0, |kb| kb / 1024.0)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let root = PathBuf::from(
|
||||
std::env::args()
|
||||
.nth(1)
|
||||
.ok_or("usage: resource_memory RESOURCES")?,
|
||||
);
|
||||
println!(
|
||||
"{:<24} {:>8} {:>8} {:>10} {:>8}",
|
||||
"step", "live MB", "+live", "step peak", "RSS MB"
|
||||
);
|
||||
let mut last = 0usize;
|
||||
let mut step = |name: &str| {
|
||||
let live = LIVE.load(Relaxed);
|
||||
let peak = PEAK.swap(live, Relaxed);
|
||||
println!(
|
||||
"{name:<24} {:>8.0} {:>+8.0} {:>10.0} {:>8.0}",
|
||||
live as f64 / MB,
|
||||
(live as f64 - last as f64) / MB,
|
||||
peak as f64 / MB,
|
||||
rss_mb()
|
||||
);
|
||||
last = live;
|
||||
};
|
||||
step("start");
|
||||
let registry = horizon_world::level::data::load_registry(&root)?;
|
||||
step("registry alone");
|
||||
drop(registry);
|
||||
step("registry dropped");
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
step("assets");
|
||||
println!(
|
||||
" sprites: {} layers of {}px, {:.0} MB of pixels",
|
||||
render.texture_layers,
|
||||
render.sprite_size,
|
||||
render.pixels.len() as f64 / MB
|
||||
);
|
||||
// The game decodes gameplay facts once for both tables (BlockFacts::load_pair).
|
||||
let (facts, behaviours) = BlockFacts::load_pair(&root, assets.registry)?;
|
||||
step("block facts+behaviours");
|
||||
let items = Items::load(&root, assets.registry, &facts)?;
|
||||
step("items");
|
||||
let recipes = Recipes::load(&root, &items)?;
|
||||
step("recipes");
|
||||
let biomes = Biomes::load(&root)?;
|
||||
step("biomes");
|
||||
let light = LightTables::load(&root, assets.registry, &facts)?;
|
||||
step("light tables");
|
||||
let tables = MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(&root, assets.registry)?,
|
||||
)?;
|
||||
step("mesh tables");
|
||||
let collision = collisions::load(&root, assets.registry)?;
|
||||
step("collision");
|
||||
let motion = MotionResources::load_with_facts(&root, &assets, &facts)?;
|
||||
step("motion");
|
||||
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, 1234)?;
|
||||
step("worldgen");
|
||||
println!(
|
||||
"{} block states; live {:.0} MB, RSS {:.0} MB",
|
||||
facts.states.len(),
|
||||
LIVE.load(Relaxed) as f64 / MB,
|
||||
rss_mb()
|
||||
);
|
||||
if let Ok(out) = Command::new("footprint")
|
||||
.arg(std::process::id().to_string())
|
||||
.output()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
for line in text
|
||||
.lines()
|
||||
.filter(|l| l.contains("Footprint") || l.contains("phys_footprint"))
|
||||
{
|
||||
println!(" {}", line.trim());
|
||||
}
|
||||
}
|
||||
// Keep everything alive until the end.
|
||||
drop((
|
||||
terrain, behaviours, motion, collision, tables, light, biomes, recipes, items, facts,
|
||||
assets,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
//! Exact NBT equality is always validated. No GPU, compression or disk I/O timed.
|
||||
use horizon_client::{
|
||||
biome::Biomes,
|
||||
chunk::{Chunk, block_strategy},
|
||||
chunk::{BLOCK_STRATEGY, Chunk},
|
||||
containers::BlockEntity,
|
||||
items::ItemStack,
|
||||
save::{self, ChunkExtras},
|
||||
@@ -64,8 +64,9 @@ fn fixture(per_chunk: usize) -> (Vec<Arc<Chunk>>, Entities, Names) {
|
||||
};
|
||||
let mut template = Chunk::empty(0, 0);
|
||||
for s in &mut template.sections[..8] {
|
||||
Arc::make_mut(s).blocks = PalettedContainer::new(block_strategy(), 1);
|
||||
Arc::make_mut(s).blocks = PalettedContainer::new(BLOCK_STRATEGY, 1);
|
||||
}
|
||||
template.heights = Arc::new([64; 256]);
|
||||
for i in 0..256 {
|
||||
let x = i % 16 - 8;
|
||||
let z = i / 16 - 8;
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
//! The region writer on real chunks, without a window: how long a world's
|
||||
//! chunks take to be encoded, compressed and written by `SaveDir`, against
|
||||
//! the same encoding and compression done one chunk after another (what the
|
||||
//! writer thread did alone before it had helpers).
|
||||
//!
|
||||
//! save_bench RESOURCES DIMENSION_DIR [--rounds N]
|
||||
//!
|
||||
//! DIMENSION_DIR is a saved dimension folder (`saves/W/dimensions/minecraft/
|
||||
//! overworld`); its chunks are read once, then each round saves them all into
|
||||
//! a scratch folder with `save_later` + `flush`, and times the one-thread
|
||||
//! baseline. The written chunks are checked byte for byte against the
|
||||
//! baseline's compressed records.
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
save::{SaveDir, SaveTables, encode_with},
|
||||
};
|
||||
use horizon_core::position::ChunkPos;
|
||||
use horizon_world::{
|
||||
compression::Compression,
|
||||
nbt::Limits,
|
||||
region::{ChunkRecord, RegionFile},
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc, time::Instant};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let usage = "usage: save_bench RESOURCES DIMENSION_DIR [--rounds N]";
|
||||
let root = PathBuf::from(args.first().ok_or(usage)?);
|
||||
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
|
||||
let rounds: usize = args
|
||||
.iter()
|
||||
.position(|a| a == "--rounds")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5);
|
||||
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Arc::new(Biomes::load(&root)?);
|
||||
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
|
||||
let registry = assets.registry;
|
||||
|
||||
let source = SaveDir::new(dimension.clone());
|
||||
let _ = source.tables.set(tables.clone());
|
||||
let mut chunks = Vec::new();
|
||||
for entry in std::fs::read_dir(dimension.join("region"))? {
|
||||
let path = entry?.path();
|
||||
if path.extension().is_none_or(|e| e != "mca") {
|
||||
continue;
|
||||
}
|
||||
let region = RegionFile::open(&path)?;
|
||||
for (pos, _) in region.chunks() {
|
||||
if let Some(chunk) = source.load(pos.x, pos.z, registry, &biomes) {
|
||||
chunks.push(Arc::new(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
chunks.sort_by_key(|c| (c.x, c.z));
|
||||
println!("{} chunks from {}", chunks.len(), dimension.display());
|
||||
|
||||
let scratch = std::env::temp_dir().join(format!("save_bench_{}", std::process::id()));
|
||||
// Encoding and compressing on one thread (what the writer did alone),
|
||||
// then SaveDir's writer with its helpers.
|
||||
let mut times = [Vec::new(), Vec::new(), Vec::new()];
|
||||
let mut written = Vec::new();
|
||||
for round in 0..rounds {
|
||||
let started = Instant::now();
|
||||
let tags: Vec<_> = chunks
|
||||
.iter()
|
||||
.map(|c| encode_with(c, registry, &biomes, Some(&tables), true))
|
||||
.collect();
|
||||
times[0].push(started.elapsed().as_secs_f64());
|
||||
let started = Instant::now();
|
||||
let baseline: Vec<ChunkRecord> = tags
|
||||
.iter()
|
||||
.map(|tag| ChunkRecord::from_nbt(tag, Compression::Zlib, 0, Limits::default()))
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
times[1].push(started.elapsed().as_secs_f64());
|
||||
drop(tags);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
let save = SaveDir::new(scratch.clone());
|
||||
let _ = save.tables.set(tables.clone());
|
||||
let started = Instant::now();
|
||||
save.save_later(&chunks, registry, &biomes)?;
|
||||
save.flush().unwrap();
|
||||
times[2].push(started.elapsed().as_secs_f64());
|
||||
drop(save);
|
||||
|
||||
if round == 0 {
|
||||
for (chunk, want) in chunks.iter().zip(&baseline) {
|
||||
let path = scratch.join(format!("region/r.{}.{}.mca", chunk.x >> 5, chunk.z >> 5));
|
||||
let got = RegionFile::open(&path)?
|
||||
.read(ChunkPos::new(chunk.x, chunk.z))?
|
||||
.ok_or("a saved chunk is missing")?;
|
||||
if got.compressed != want.compressed {
|
||||
return Err(format!("chunk {} {} differs", chunk.x, chunk.z).into());
|
||||
}
|
||||
}
|
||||
written = baseline.iter().map(|r| r.compressed.len()).collect();
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
println!(
|
||||
"{} KiB compressed, identical to the one-thread records",
|
||||
written.iter().sum::<usize>() / 1024
|
||||
);
|
||||
let per_chunk = 1000.0 / chunks.len() as f64;
|
||||
let names = [
|
||||
"one thread: encode",
|
||||
"one thread: compress",
|
||||
"SaveDir (encode, compress, write)",
|
||||
];
|
||||
for (name, t) in names.iter().zip(&mut times) {
|
||||
t.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"{name}: best {:.0} ms, median {:.0} ms of {rounds} ({:.3} ms per chunk at best)",
|
||||
t[0] * 1000.0,
|
||||
t[t.len() / 2] * 1000.0,
|
||||
t[0] * per_chunk
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -257,9 +257,9 @@ fn main() -> horizon_client::Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
fn tables_air(assets: &Assets) -> u32 {
|
||||
fn tables_air(assets: &Assets) -> u16 {
|
||||
assets
|
||||
.registry
|
||||
.default_state("minecraft:air")
|
||||
.map_or(0, |s| s.0)
|
||||
.map_or(0, |s| s.0 as u16)
|
||||
}
|
||||
@@ -40,8 +40,7 @@ fn main() -> Result<()> {
|
||||
return Err("radius must be 1..16".into());
|
||||
}
|
||||
let assets = assets::Assets::load(&resources)?;
|
||||
let render = assets.reload(&resources)?;
|
||||
let world = world::World::test_scene(&assets, &render, radius)?;
|
||||
let world = world::World::test_scene(&assets, radius)?;
|
||||
let binding = collisions::load(&resources, assets.registry)?;
|
||||
let mut scene = collisions::SceneCollisions {
|
||||
world: &world,
|
||||
@@ -51,7 +50,7 @@ fn main() -> Result<()> {
|
||||
fs::write(
|
||||
dir.join("scene.json"),
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"radius":radius,"resident_chunks":world.chunks.len(),"model_gallery":render.model_gallery,
|
||||
"radius":radius,"resident_chunks":world.chunks.len(),"model_gallery":assets.model_gallery,
|
||||
}))?,
|
||||
)?;
|
||||
let mut output = std::io::BufWriter::new(fs::File::create(dir.join("rust.txt"))?);
|
||||
|
||||
@@ -243,14 +243,13 @@ fn main() -> Result<()> {
|
||||
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)?;
|
||||
let binding = collisions::load(&root, assets.registry)?;
|
||||
let resources = MotionResources::load(&root, &assets)?;
|
||||
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" {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! the oracle's canonical form (vanilla ARGB colours and packed light coords).
|
||||
#![forbid(unsafe_code)]
|
||||
use horizon_client::{
|
||||
assets::{Assets, RenderAssets},
|
||||
assets::Assets,
|
||||
biome::{BiomeInfo, Biomes, GrassModifier},
|
||||
blocks::BlockFacts,
|
||||
chunk::{Chunk, Light, MIN_Y, index},
|
||||
@@ -48,7 +48,6 @@ fn chunk_index(x: i32, z: i32) -> Result<usize> {
|
||||
fn scene(
|
||||
row: &Value,
|
||||
assets: &Assets,
|
||||
render: &RenderAssets,
|
||||
tables: &MeshTables,
|
||||
biomes: &mut Biomes,
|
||||
pristine: &[BiomeInfo],
|
||||
@@ -84,7 +83,7 @@ fn scene(
|
||||
for block in row["blocks"].as_array().ok_or("missing blocks")? {
|
||||
let (x, y, z) = (int(&block[0])?, int(&block[1])?, int(&block[2])?);
|
||||
let i = chunk_index(x, z)?;
|
||||
chunks[i].set(x & 15, y, z & 15, u32::try_from(int(&block[3])?)?);
|
||||
chunks[i].set(x & 15, y, z & 15, u16::try_from(int(&block[3])?)?);
|
||||
}
|
||||
for cell in row["lights"].as_array().ok_or("missing lights")? {
|
||||
let (x, y, z) = (int(&cell[0])?, int(&cell[1])?, int(&cell[2])?);
|
||||
@@ -128,7 +127,6 @@ fn scene(
|
||||
let mut mesher = Mesher {
|
||||
nether_shading: row["nether"].as_bool().unwrap_or(false),
|
||||
assets,
|
||||
render,
|
||||
tables,
|
||||
biomes: &*biomes,
|
||||
visibility: tables.visibility.clone(),
|
||||
@@ -161,8 +159,15 @@ fn scene(
|
||||
let vertices: Vec<Value> = quad
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let ([x, y, z], [u, w]) = (v.pos, v.uv());
|
||||
json!([x, y, z, u, w, argb(v.color), light_coords(v.light)])
|
||||
json!([
|
||||
v.pos[0],
|
||||
v.pos[1],
|
||||
v.pos[2],
|
||||
v.uv[0],
|
||||
v.uv[1],
|
||||
argb(v.color),
|
||||
light_coords(v.light)
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
quads.push(json!({"layer": layer, "sprite": sprite, "vertices": vertices}));
|
||||
@@ -179,12 +184,11 @@ fn main() -> Result<()> {
|
||||
}
|
||||
let root = PathBuf::from(&args[0]);
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let mut biomes = Biomes::load(&root)?;
|
||||
let visibility = models::load_visibility(&root, assets.registry)?;
|
||||
let tables = MeshTables::new(&assets, &render, &facts, visibility)?;
|
||||
let names: BTreeMap<u32, &str> = render
|
||||
let tables = MeshTables::new(&assets, &facts, visibility)?;
|
||||
let names: BTreeMap<u32, &str> = assets
|
||||
.sprite_layers
|
||||
.iter()
|
||||
.map(|(name, layer)| (*layer, name.as_str()))
|
||||
@@ -218,7 +222,6 @@ fn main() -> Result<()> {
|
||||
let result = scene(
|
||||
&row,
|
||||
&assets,
|
||||
&render,
|
||||
&tables,
|
||||
&mut biomes,
|
||||
&pristine,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
//! what the render thread pays for it in `Streaming::update`.
|
||||
//!
|
||||
//! streaming_bench RESOURCES [--threads N] [--radius R] [--seed S] [--fly CHUNKS_PER_S] [--seconds T] [--hz F]
|
||||
//! [--dimension overworld|the_nether|the_end] [--center-x X] [--center-z Z]
|
||||
//!
|
||||
//! First the area around (0, 0) fills; then, with `--fly`, the centre moves along +x
|
||||
//! at that speed for T seconds while frames run at F per second. Each phase reports
|
||||
@@ -73,33 +72,14 @@ static ALLOCATOR: CountingAllocator = CountingAllocator;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn cpu_seconds() -> f64 {
|
||||
std::fs::read_to_string("/proc/self/stat")
|
||||
.ok()
|
||||
.and_then(|stat| {
|
||||
let fields = stat
|
||||
.rsplit_once(')')?
|
||||
.1
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>();
|
||||
Some((fields[11].parse::<f64>().ok()? + fields[12].parse::<f64>().ok()?) / 100.0)
|
||||
})
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Phase {
|
||||
updates: Vec<f64>,
|
||||
local: (u64, u64),
|
||||
uploads: usize,
|
||||
stage_samples: [u64; 7],
|
||||
cpu_start: f64,
|
||||
}
|
||||
impl Phase {
|
||||
fn update(&mut self, streaming: &mut Streaming, world: &mut World) {
|
||||
if self.updates.is_empty() {
|
||||
self.cpu_start = cpu_seconds();
|
||||
}
|
||||
let before = LOCAL.with(Cell::get);
|
||||
let started = Instant::now();
|
||||
let (uploads, _) = streaming.update(world);
|
||||
@@ -109,9 +89,6 @@ impl Phase {
|
||||
self.local.1 += after.1 - before.1;
|
||||
self.uploads += uploads.len();
|
||||
drop(uploads);
|
||||
for (sum, (_, count)) in self.stage_samples.iter_mut().zip(streaming.stages()) {
|
||||
*sum += count as u64;
|
||||
}
|
||||
}
|
||||
fn report(&self, name: &str, wall: Duration, streaming: &Streaming, generated: u64) {
|
||||
let mut sorted = self.updates.clone();
|
||||
@@ -129,16 +106,6 @@ impl Phase {
|
||||
at(0.99),
|
||||
sorted.last().copied().unwrap_or(0.0),
|
||||
);
|
||||
println!(
|
||||
"BENCH {}",
|
||||
serde_json::json!({
|
||||
"phase": name, "wall_s": wall.as_secs_f64(),
|
||||
"cpu_s": cpu_seconds() - self.cpu_start,
|
||||
"chunks": streaming.generated_total - generated, "sections": self.uploads,
|
||||
"updates": self.updates.len(), "update_ms": total,
|
||||
"mean_stages": self.stage_samples.map(|n| n as f64 / self.updates.len() as f64),
|
||||
})
|
||||
);
|
||||
if TRACK {
|
||||
println!(
|
||||
" update allocations: {} ({:.1} MiB)",
|
||||
@@ -175,44 +142,24 @@ fn main() -> Result<()> {
|
||||
let fly = flag("--fly", 0.0);
|
||||
let seconds = flag("--seconds", 10.0);
|
||||
let hz = flag("--hz", 240.0);
|
||||
let dimension = args
|
||||
.iter()
|
||||
.position(|a| a == "--dimension")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.map(|d| {
|
||||
if d.contains(':') {
|
||||
d.clone()
|
||||
} else {
|
||||
format!("minecraft:{d}")
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "minecraft:overworld".into());
|
||||
let center = (
|
||||
flag("--center-x", 0.0) as i32,
|
||||
flag("--center-z", 0.0) as i32,
|
||||
);
|
||||
|
||||
let assets = Arc::new(Assets::load(&root)?);
|
||||
let render = Arc::new(assets.reload(&root)?);
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Arc::new(Biomes::load(&root)?);
|
||||
let tables = Arc::new(MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(&root, assets.registry)?,
|
||||
)?);
|
||||
let light = Arc::new(LightTables::load(&root, assets.registry, &facts)?);
|
||||
let terrain = Arc::new(VanillaTerrain::for_dimension(
|
||||
let terrain = Arc::new(VanillaTerrain::new(
|
||||
&root.join("worldgen"),
|
||||
assets.registry,
|
||||
&biomes,
|
||||
seed,
|
||||
&dimension,
|
||||
)?);
|
||||
let context = Arc::new(WorkerContext {
|
||||
assets,
|
||||
render: render.clone(),
|
||||
tables,
|
||||
biomes,
|
||||
light,
|
||||
@@ -220,12 +167,8 @@ fn main() -> Result<()> {
|
||||
let mut world = World::new(facts.air, seed);
|
||||
let mut streaming = Streaming::new(terrain, context, threads);
|
||||
streaming.radius = radius;
|
||||
streaming.center = center;
|
||||
streaming.nether_shading = dimension == "minecraft:the_nether";
|
||||
streaming.sky_light = dimension != "minecraft:the_nether";
|
||||
println!(
|
||||
"{threads} workers, radius {radius}, seed {seed}, dimension {dimension}, center {center:?}"
|
||||
);
|
||||
streaming.center = (0, 0);
|
||||
println!("{threads} workers, radius {radius}, seed {seed}");
|
||||
let (allocations, bytes) = (ALLOCATIONS.load(Relaxed), BYTES.load(Relaxed));
|
||||
|
||||
// Fill: update as often as the game would uncapped, until settled.
|
||||
@@ -252,12 +195,12 @@ fn main() -> Result<()> {
|
||||
let mut behind = Vec::new();
|
||||
while started.elapsed().as_secs_f64() < seconds {
|
||||
let next = Instant::now() + frame;
|
||||
let x = center.0 + (started.elapsed().as_secs_f64() * fly) as i32;
|
||||
streaming.center = (x, center.1);
|
||||
let x = (started.elapsed().as_secs_f64() * fly) as i32;
|
||||
streaming.center = (x, 0);
|
||||
phase.update(&mut streaming, &mut world);
|
||||
// Columns within 2 chunks of the player still missing their meshes.
|
||||
let near = (-2..=2)
|
||||
.flat_map(|dz| (-2..=2).map(move |dx| (x + dx, center.1 + dz)))
|
||||
.flat_map(|dz| (-2..=2).map(move |dx| (x + dx, dz)))
|
||||
.filter(|&(cx, cz)| !streaming.is_lit(cx, cz))
|
||||
.count();
|
||||
behind.push(near);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
//! Tooltip display visibility oracle companion for tools/test_tooltip_display_parity.py.
|
||||
use horizon_client::{item_tooltip::TooltipDisplay, items::ItemStack};
|
||||
use horizon_world::nbt::Tag;
|
||||
use std::{error::Error, fmt::Write};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut output = String::new();
|
||||
for line in std::fs::read_to_string(&args[1])?.lines() {
|
||||
let fields: Vec<&str> = line.split('\t').collect();
|
||||
let hide_tooltip: bool = fields[0].parse()?;
|
||||
let creative: bool = fields[1].parse()?;
|
||||
let hide_attributes: bool = fields[2].parse()?;
|
||||
let mut component = Vec::new();
|
||||
if hide_tooltip {
|
||||
component.push(("hide_tooltip", Tag::Byte(1)));
|
||||
}
|
||||
if hide_attributes {
|
||||
component.push((
|
||||
"hidden_components",
|
||||
Tag::List(vec![Tag::String("minecraft:attribute_modifiers".into())]),
|
||||
));
|
||||
}
|
||||
let mut stack = ItemStack::new("minecraft:diamond_sword", 1);
|
||||
if !component.is_empty() {
|
||||
stack
|
||||
.data_mut()
|
||||
.set_other("minecraft:tooltip_display", Some(Tag::compound(component)));
|
||||
}
|
||||
let display = TooltipDisplay::for_stack(&stack);
|
||||
writeln!(
|
||||
output,
|
||||
"{}\t{}",
|
||||
display.name_visible(creative),
|
||||
display.shows("minecraft:attribute_modifiers")
|
||||
)?;
|
||||
}
|
||||
std::fs::write(&args[2], output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -239,12 +239,10 @@ fn main() -> horizon_client::Result<()> {
|
||||
let vanilla = fs::read_to_string(args.get(3).ok_or(usage)?)?;
|
||||
let items = Items::catalog(root)?;
|
||||
let enchantments = Enchantments::load(root);
|
||||
let names = horizon_client::creative::Names::default();
|
||||
let trades = Trades::load(root);
|
||||
let recipes = horizon_client::crafting::Recipes::load(root, &items)?;
|
||||
let brewing = horizon_client::brewing::Brewing::load(root, &items);
|
||||
let banners = horizon_client::banner::Banners::load(root);
|
||||
let station_recipes = horizon_client::stations::StationRecipes::default();
|
||||
let types: HashMap<_, _> = horizon_client::mobs::MobTypes::load(root)?
|
||||
.types
|
||||
.into_iter()
|
||||
@@ -380,11 +378,7 @@ fn main() -> horizon_client::Result<()> {
|
||||
recipes: &recipes,
|
||||
brewing: &brewing,
|
||||
banners: &banners,
|
||||
station_recipes: &station_recipes,
|
||||
enchantments: &enchantments,
|
||||
names: &names,
|
||||
trade: Some(&env_trade),
|
||||
maps: None,
|
||||
};
|
||||
trade(&mut out, name, &kv, &mut world, &env, origin)?;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ fn bits(value: f32) -> String {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let mut output = String::new();
|
||||
let mut parameters = weather::ParameterCache::default();
|
||||
for (case, line) in fs::read_to_string(&args[1])?.lines().enumerate() {
|
||||
let p: Vec<&str> = line.split_whitespace().collect();
|
||||
let int = |i: usize| p[i].parse::<i32>().unwrap();
|
||||
@@ -39,13 +38,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
bits(column.v_offset),
|
||||
column.light_coords
|
||||
)?;
|
||||
assert_eq!(
|
||||
parameters
|
||||
.get(x, z)
|
||||
.column(kind != 0, ticks, x, bottom, top, z, light, partial),
|
||||
column,
|
||||
"cached parameters differ from original random source at case {case}"
|
||||
);
|
||||
let max_alpha = if kind == 0 { 1.0 } else { 0.8 };
|
||||
for v in weather::column_quad(&column, camera, max_alpha, radius, intensity) {
|
||||
write!(
|
||||
|
||||
@@ -53,10 +53,6 @@ pub struct Tree {
|
||||
pub by_id: HashMap<String, usize>,
|
||||
/// trigger id -> (advancement index, criterion index).
|
||||
pub by_trigger: HashMap<String, Vec<(usize, usize)>>,
|
||||
/// `recipe_unlocked` criteria by the recipe they name (`index_recipes`);
|
||||
/// those naming none in `recipes_any`.
|
||||
by_recipe: HashMap<String, Vec<(usize, usize)>>,
|
||||
recipes_any: Vec<(usize, usize)>,
|
||||
}
|
||||
fn frame(v: &Value) -> Frame {
|
||||
match v.as_str() {
|
||||
@@ -97,111 +93,12 @@ impl Tree {
|
||||
tree.list.push(a);
|
||||
}
|
||||
}
|
||||
tree.index_recipes();
|
||||
tree
|
||||
}
|
||||
/// Index the `recipe_unlocked` criteria by recipe: a grant that unlocks
|
||||
/// twenty recipes tested every one of the ~1,300 recipe advancements'
|
||||
/// criteria for each (several ms in one tick).
|
||||
pub fn index_recipes(&mut self) {
|
||||
self.by_recipe.clear();
|
||||
self.recipes_any.clear();
|
||||
for &(ai, ci) in self
|
||||
.by_trigger
|
||||
.get("minecraft:recipe_unlocked")
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
match self.list[ai].criteria[ci].conditions["recipes"].as_str() {
|
||||
Some(r) => self
|
||||
.by_recipe
|
||||
.entry(r.to_owned())
|
||||
.or_default()
|
||||
.push((ai, ci)),
|
||||
None => self.recipes_any.push((ai, ci)),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The `recipe_unlocked` criteria that could pass for `recipe`, in tree
|
||||
/// order (the others name another recipe).
|
||||
pub fn recipe_criteria(&self, recipe: &str) -> Vec<(usize, usize)> {
|
||||
let mut out = self.recipes_any.clone();
|
||||
out.extend(self.by_recipe.get(recipe).into_iter().flatten());
|
||||
out.sort_unstable();
|
||||
out
|
||||
}
|
||||
pub fn get(&self, id: &str) -> Option<&Advancement> {
|
||||
self.by_id.get(id).map(|&i| &self.list[i])
|
||||
}
|
||||
}
|
||||
|
||||
/// The `inventory_changed` criteria by the items they need. A criterion whose
|
||||
/// item predicates include one limited to some items (an id, a `#tag`, a
|
||||
/// list) can only pass with one of those in the inventory, so an inventory
|
||||
/// change tests only the criteria of the items it holds, plus those that
|
||||
/// need no particular item. The same answers as testing them all: the ~1,300
|
||||
/// recipe advancements each have one, and testing every one of them cost a
|
||||
/// player 1–4 ms on every tick its inventory changed.
|
||||
#[derive(Default)]
|
||||
pub struct InventoryIndex {
|
||||
by_item: HashMap<String, Vec<(usize, usize)>>,
|
||||
any: Vec<(usize, usize)>,
|
||||
}
|
||||
impl InventoryIndex {
|
||||
/// `tag` gives a `#tag`'s item ids (none for a tag that doesn't exist,
|
||||
/// which matches nothing).
|
||||
pub fn build<'t>(tree: &Tree, tag: impl Fn(&str) -> Vec<&'t str>) -> Self {
|
||||
fn ids<'t>(v: &Value, tag: &impl Fn(&str) -> Vec<&'t str>, out: &mut Vec<String>) {
|
||||
match v {
|
||||
Value::String(s) => match s.strip_prefix('#') {
|
||||
Some(t) => out.extend(tag(t).into_iter().map(str::to_owned)),
|
||||
None => out.push(s.clone()),
|
||||
},
|
||||
Value::Array(a) => a.iter().for_each(|x| ids(x, tag, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut index = Self::default();
|
||||
let Some(list) = tree.by_trigger.get("minecraft:inventory_changed") else {
|
||||
return index;
|
||||
};
|
||||
for &(ai, ci) in list {
|
||||
let items = &tree.list[ai].criteria[ci].conditions["items"];
|
||||
// `id_matches`: only a string or a list limits the items.
|
||||
let limited = items.as_array().and_then(|a| {
|
||||
a.iter()
|
||||
.map(|p| &p["items"])
|
||||
.find(|v| v.is_string() || v.is_array())
|
||||
});
|
||||
match limited {
|
||||
Some(v) => {
|
||||
let mut out = Vec::new();
|
||||
ids(v, &tag, &mut out);
|
||||
out.sort();
|
||||
out.dedup();
|
||||
for id in out {
|
||||
index.by_item.entry(id).or_default().push((ai, ci));
|
||||
}
|
||||
}
|
||||
None => index.any.push((ai, ci)),
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
/// The criteria an inventory holding these item ids could pass, in
|
||||
/// tree order.
|
||||
pub fn candidates<'a>(&self, held: impl IntoIterator<Item = &'a str>) -> Vec<(usize, usize)> {
|
||||
let mut out = self.any.clone();
|
||||
for id in held {
|
||||
if let Some(list) = self.by_item.get(id) {
|
||||
out.extend_from_slice(list);
|
||||
}
|
||||
}
|
||||
out.sort_unstable();
|
||||
out.dedup();
|
||||
out
|
||||
}
|
||||
}
|
||||
fn collect(root: &Path, dir: &Path, ns: &str, out: &mut Vec<(String, std::path::PathBuf)>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
@@ -710,29 +607,6 @@ pub struct Entities<'a> {
|
||||
pub tool: Option<&'a ItemView>,
|
||||
pub block_pos: Option<[i32; 3]>,
|
||||
}
|
||||
/// Conditions shared by item/entity interactions and thrown-item pickups.
|
||||
pub fn item_entity(
|
||||
v: &Value,
|
||||
player: &EntityView,
|
||||
target: &EntityView,
|
||||
stack: &ItemView,
|
||||
w: &dyn World,
|
||||
) -> bool {
|
||||
let pctx = Entities {
|
||||
this: Some(player),
|
||||
origin: player.position,
|
||||
tool: None,
|
||||
block_pos: None,
|
||||
};
|
||||
let ectx = Entities {
|
||||
this: Some(target),
|
||||
..pctx
|
||||
};
|
||||
conditions(&v["player"], &pctx, w)
|
||||
&& conditions(&v["entity"], &ectx, w)
|
||||
&& item(&v["item"], stack, w)
|
||||
}
|
||||
|
||||
/// A `ContextAwarePredicate`: a list of loot conditions (or one condition).
|
||||
pub fn conditions(v: &Value, ctx: &Entities, w: &dyn World) -> bool {
|
||||
match v {
|
||||
@@ -1095,77 +969,6 @@ impl Tree {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The criteria an inventory change and a recipe unlock test, on vanilla's tree:
|
||||
/// `cargo test -p horizon-client --lib -- --ignored --nocapture index_on_vanilla_tree`
|
||||
/// with HORIZON_RESOURCES set to the prepared resources.
|
||||
#[test]
|
||||
#[ignore = "needs the prepared resources"]
|
||||
fn index_on_vanilla_tree() {
|
||||
let Some(root) = std::env::var_os("HORIZON_RESOURCES").map(std::path::PathBuf::from) else {
|
||||
return;
|
||||
};
|
||||
let data = root.join("data");
|
||||
let t = std::time::Instant::now();
|
||||
let tree = Tree::load(&data);
|
||||
let load = t.elapsed();
|
||||
let tags = data.join("minecraft/tags/item");
|
||||
let tag_items: HashMap<String, Vec<String>> = std::fs::read_dir(&tags)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|e| {
|
||||
let v: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(e.path()).ok()?).ok()?;
|
||||
let name = format!("minecraft:{}", e.path().file_stem()?.to_string_lossy());
|
||||
let values = v["values"]
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|x| x.as_str().map(str::to_owned));
|
||||
Some((name, values.collect()))
|
||||
})
|
||||
.collect();
|
||||
let t = std::time::Instant::now();
|
||||
let index = InventoryIndex::build(&tree, |tag| {
|
||||
tag_items
|
||||
.get(tag)
|
||||
.map(|v| v.iter().map(String::as_str).collect())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let build = t.elapsed();
|
||||
let all = tree
|
||||
.by_trigger
|
||||
.get("minecraft:inventory_changed")
|
||||
.map_or(0, Vec::len);
|
||||
let held = [
|
||||
"minecraft:iron_ingot",
|
||||
"minecraft:oak_log",
|
||||
"minecraft:cobblestone",
|
||||
"minecraft:diamond_pickaxe",
|
||||
"minecraft:bread",
|
||||
"minecraft:torch",
|
||||
"minecraft:oak_planks",
|
||||
"minecraft:stick",
|
||||
"minecraft:coal",
|
||||
];
|
||||
let t = std::time::Instant::now();
|
||||
let mut candidates = 0;
|
||||
for _ in 0..1000 {
|
||||
candidates = index.candidates(held).len();
|
||||
}
|
||||
let per_change = t.elapsed() / 1000;
|
||||
let recipes = tree
|
||||
.by_trigger
|
||||
.get("minecraft:recipe_unlocked")
|
||||
.map_or(0, Vec::len);
|
||||
let per_recipe = tree.recipe_criteria("minecraft:iron_pickaxe").len();
|
||||
eprintln!(
|
||||
"tree {load:?}; inventory_changed: {all} criteria, index built in {build:?}, \
|
||||
a 9-stack inventory tests {candidates} (looked up in {per_change:?}); \
|
||||
recipe_unlocked: {recipes} criteria, iron_pickaxe tests {per_recipe}"
|
||||
);
|
||||
assert!(candidates < all / 10);
|
||||
}
|
||||
struct Flat;
|
||||
impl World for Flat {
|
||||
fn dimension(&self) -> &str {
|
||||
@@ -1193,166 +996,10 @@ mod tests {
|
||||
false
|
||||
}
|
||||
fn in_tag(&self, _: &str, tag: &str, id: &str) -> bool {
|
||||
(tag == "minecraft:stone_tool_materials" && id == "minecraft:cobblestone")
|
||||
|| (tag == "minecraft:sulfur_cube_archetype/explosive" && id == "minecraft:tnt")
|
||||
tag == "minecraft:stone_tool_materials" && id == "minecraft:cobblestone"
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn uh_oh_accepts_either_tnt_event_only_for_adult_sulfur_cubes() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"../../.local/horizon-client/26.3/data/minecraft/advancement/husbandry/uh_oh.json",
|
||||
);
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
let json = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
|
||||
let advancement = parse("minecraft:husbandry/uh_oh", &json).unwrap();
|
||||
assert_eq!(advancement.criteria.len(), 2);
|
||||
let player = EntityView {
|
||||
kind: "minecraft:player".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut target = EntityView {
|
||||
kind: "minecraft:sulfur_cube".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut stack = ItemView {
|
||||
id: "minecraft:tnt".into(),
|
||||
count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
for criterion in &advancement.criteria {
|
||||
assert!(item_entity(
|
||||
&criterion.conditions,
|
||||
&player,
|
||||
&target,
|
||||
&stack,
|
||||
&Flat
|
||||
));
|
||||
let mut progress = Progress::default();
|
||||
progress.grant(&advancement.id, &criterion.name, "test");
|
||||
assert!(
|
||||
progress.done(&advancement),
|
||||
"either criterion must award Uh Oh"
|
||||
);
|
||||
target.baby = true;
|
||||
assert!(!item_entity(
|
||||
&criterion.conditions,
|
||||
&player,
|
||||
&target,
|
||||
&stack,
|
||||
&Flat
|
||||
));
|
||||
target.baby = false;
|
||||
target.kind = "minecraft:pig".into();
|
||||
assert!(!item_entity(
|
||||
&criterion.conditions,
|
||||
&player,
|
||||
&target,
|
||||
&stack,
|
||||
&Flat
|
||||
));
|
||||
target.kind = "minecraft:sulfur_cube".into();
|
||||
stack.id = "minecraft:stone".into();
|
||||
assert!(!item_entity(
|
||||
&criterion.conditions,
|
||||
&player,
|
||||
&target,
|
||||
&stack,
|
||||
&Flat
|
||||
));
|
||||
stack.id = "minecraft:tnt".into();
|
||||
}
|
||||
}
|
||||
/// Only the criteria the held items could pass are tested; those that
|
||||
/// need no particular item always are.
|
||||
#[test]
|
||||
fn inventory_index_keeps_the_criteria_the_items_could_pass() {
|
||||
let criteria = [
|
||||
serde_json::json!({"items": [{"items": "minecraft:diamond"}]}),
|
||||
serde_json::json!({"items": [{"items": "#minecraft:planks"}]}),
|
||||
serde_json::json!({"items": [{"count": {"min": 2}}]}),
|
||||
serde_json::json!({"items": [{"count": 1}, {"items": ["minecraft:stick", "minecraft:apple"]}]}),
|
||||
serde_json::json!({"slots": {"empty": 0}}),
|
||||
serde_json::json!({"items": [{"items": "#minecraft:no_such_tag"}]}),
|
||||
];
|
||||
let mut tree = Tree::default();
|
||||
for (i, conditions) in criteria.into_iter().enumerate() {
|
||||
tree.list.push(Advancement {
|
||||
id: format!("test:{i}"),
|
||||
parent: None,
|
||||
display: None,
|
||||
criteria: vec![Criterion {
|
||||
name: "c".into(),
|
||||
trigger: "minecraft:inventory_changed".into(),
|
||||
conditions,
|
||||
}],
|
||||
requirements: vec![vec!["c".into()]],
|
||||
rewards: Rewards::default(),
|
||||
});
|
||||
tree.by_trigger
|
||||
.entry("minecraft:inventory_changed".into())
|
||||
.or_default()
|
||||
.push((i, 0));
|
||||
}
|
||||
let index = InventoryIndex::build(&tree, |tag| match tag {
|
||||
"minecraft:planks" => vec!["minecraft:oak_planks", "minecraft:birch_planks"],
|
||||
_ => Vec::new(),
|
||||
});
|
||||
let found = |held: &[&str]| -> Vec<usize> {
|
||||
index
|
||||
.candidates(held.iter().copied())
|
||||
.into_iter()
|
||||
.map(|(ai, _)| ai)
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(found(&[]), [2, 4]);
|
||||
assert_eq!(found(&["minecraft:dirt"]), [2, 4]);
|
||||
assert_eq!(
|
||||
found(&["minecraft:oak_planks", "minecraft:diamond"]),
|
||||
[0, 1, 2, 4]
|
||||
);
|
||||
assert_eq!(found(&["minecraft:apple", "minecraft:apple"]), [2, 3, 4]);
|
||||
}
|
||||
/// A recipe unlocked tests only the criteria that name it.
|
||||
#[test]
|
||||
fn recipe_index_keeps_the_criteria_of_the_recipe() {
|
||||
let mut tree = Tree::default();
|
||||
for (i, conditions) in [
|
||||
serde_json::json!({"recipes": "minecraft:oak_planks"}),
|
||||
serde_json::json!({"recipes": "minecraft:stick"}),
|
||||
serde_json::json!({}),
|
||||
serde_json::json!({"recipes": "minecraft:oak_planks"}),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
tree.list.push(Advancement {
|
||||
id: format!("test:{i}"),
|
||||
parent: None,
|
||||
display: None,
|
||||
criteria: vec![Criterion {
|
||||
name: "c".into(),
|
||||
trigger: "minecraft:recipe_unlocked".into(),
|
||||
conditions,
|
||||
}],
|
||||
requirements: vec![vec!["c".into()]],
|
||||
rewards: Rewards::default(),
|
||||
});
|
||||
tree.by_trigger
|
||||
.entry("minecraft:recipe_unlocked".into())
|
||||
.or_default()
|
||||
.push((i, 0));
|
||||
}
|
||||
tree.index_recipes();
|
||||
assert_eq!(
|
||||
tree.recipe_criteria("minecraft:oak_planks"),
|
||||
[(0, 0), (2, 0), (3, 0)]
|
||||
);
|
||||
assert_eq!(tree.recipe_criteria("minecraft:stick"), [(1, 0), (2, 0)]);
|
||||
assert_eq!(tree.recipe_criteria("minecraft:torch"), [(2, 0)]);
|
||||
}
|
||||
#[test]
|
||||
fn bucket_axolotl_block_advancements_require_the_placed_block() {
|
||||
let ctx = Entities {
|
||||
this: None,
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
use crate::{
|
||||
biome::Biomes,
|
||||
blocks::{BlockFacts, Fluid},
|
||||
command::args::GameMode,
|
||||
items::ItemStack,
|
||||
particles::{ParticleEngine, ParticleOptions, ParticleType as T},
|
||||
world::World,
|
||||
};
|
||||
@@ -61,8 +59,6 @@ enum Animate {
|
||||
Fire,
|
||||
/// An active sculk sensor.
|
||||
SculkSensor,
|
||||
/// Potent sulfur that is not dry.
|
||||
PotentSulfur,
|
||||
}
|
||||
fn step(facing: &str) -> [i32; 3] {
|
||||
match facing {
|
||||
@@ -130,9 +126,6 @@ fn classify(name: &str, prop: &dyn Fn(&str) -> Option<String>) -> Animate {
|
||||
}
|
||||
}
|
||||
"respawn_anchor" if prop("charges").is_some_and(|c| c != "0") => Animate::RespawnAnchor,
|
||||
"potent_sulfur" if prop("potent_sulfur_state").is_some_and(|s| s != "dry") => {
|
||||
Animate::PotentSulfur
|
||||
}
|
||||
"bubble_column" => Animate::BubbleColumn {
|
||||
drag: prop("drag").as_deref() == Some("true"),
|
||||
},
|
||||
@@ -261,24 +254,6 @@ pub struct AnimateContext<'a> {
|
||||
pub registry: &'a BlockRegistry,
|
||||
/// `Level.isRainingAt`.
|
||||
pub raining_at: &'a dyn Fn([i32; 3]) -> bool,
|
||||
/// Creative main-hand marker block and a resolver for each placed state's sprite.
|
||||
pub marker: Option<(&'a str, &'a dyn Fn(BlockStateId) -> Option<ParticleOptions>)>,
|
||||
}
|
||||
|
||||
/// `ClientLevel.getMarkerParticleTarget`: only the selected main-hand item counts.
|
||||
pub fn marker_particle_target(
|
||||
mode: GameMode,
|
||||
main_hand: Option<&ItemStack>,
|
||||
) -> Option<&'static str> {
|
||||
if mode != GameMode::Creative {
|
||||
return None;
|
||||
}
|
||||
let stack = main_hand.filter(|stack| stack.count > 0)?;
|
||||
match stack.item.as_ref() {
|
||||
"minecraft:barrier" => Some("minecraft:barrier"),
|
||||
"minecraft:light" => Some("minecraft:light"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
impl AnimateContext<'_> {
|
||||
fn state(&self, p: [i32; 3]) -> BlockStateId {
|
||||
@@ -383,12 +358,6 @@ fn do_animate_tick(
|
||||
try_spawn_drip(tables, engine, ctx, [x, y - 1, z], drip, top_solid);
|
||||
}
|
||||
}
|
||||
if let Some((block, options)) = ctx.marker
|
||||
&& ctx.registry.state(state).is_some_and(|s| s.name == block)
|
||||
&& let Some(options) = options(state)
|
||||
{
|
||||
engine.add(&options, pos.map(|v| f64::from(v) + 0.5), [0.0; 3]);
|
||||
}
|
||||
if !facts.full_collision {
|
||||
let biome = ctx
|
||||
.world
|
||||
@@ -824,26 +793,6 @@ fn block_animate_tick(
|
||||
}
|
||||
}
|
||||
}
|
||||
Animate::PotentSulfur => {
|
||||
// PotentSulfurBlock.animateTick: under a water source, two
|
||||
// bubbles and, one time in ten, the noxious gas.
|
||||
let above = ctx.facts.get(ctx.state([pos[0], pos[1] + 1, pos[2]]));
|
||||
if above.fluid == Fluid::Water && above.fluid_level == 8 && !above.fluid_falling {
|
||||
for _ in 0..2 {
|
||||
let r = engine.random();
|
||||
let p = [
|
||||
fx + f64::from(r.next_float()),
|
||||
fy + 1.0 + f64::from(r.next_float()),
|
||||
fz + f64::from(r.next_float()),
|
||||
];
|
||||
add(engine, T::SulfurBubbles, p, [0.0; 3]);
|
||||
}
|
||||
if next_int(engine.random(), 10) == 0 {
|
||||
let name = "minecraft:block.potent_sulfur.noxious_gas";
|
||||
sound(engine, name, [fx, fy, fz], 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Animate::Mycelium => {
|
||||
let r = engine.random();
|
||||
if next_int(r, 10) == 0 {
|
||||
|
||||
@@ -2,13 +2,14 @@ use crate::Result;
|
||||
use horizon_world::{
|
||||
DATA_VERSION,
|
||||
heightmap::HeightmapState,
|
||||
nbt::Tag,
|
||||
registry::{BlockRegistry, BlockStateId},
|
||||
worldgen::{tags::TagDefinitions, terrain::TerrainBlockStates},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
fs::File,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
@@ -42,46 +43,17 @@ impl HeightmapState for StateFacts {
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct Properties(Arc<Vec<StateFacts>>);
|
||||
#[cfg(test)]
|
||||
impl Properties {
|
||||
/// Heightmap facts for tests, per state: (air, has a fluid, blocks
|
||||
/// motion, blocks motion but is not leaves). State 0 is minecraft:air.
|
||||
pub(crate) fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
|
||||
Self(Arc::new(
|
||||
states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &(air, fluid, motion, no_leaves))| StateFacts {
|
||||
air,
|
||||
fluid,
|
||||
air_block: i == 0,
|
||||
motion,
|
||||
no_leaves,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
impl TerrainBlockStates for Properties {
|
||||
type State = StateFacts;
|
||||
fn heightmap_state(&self, id: BlockStateId) -> StateFacts {
|
||||
self.0[id.0 as usize]
|
||||
}
|
||||
}
|
||||
/// The block registry and the facts read from `facts.json` and the block tags:
|
||||
/// everything world generation and collisions need, with none of the block
|
||||
/// models or baked sprites.
|
||||
pub struct Assets {
|
||||
/// Leaked once per process: shared by world generation threads.
|
||||
pub registry: &'static BlockRegistry,
|
||||
pub properties: Properties,
|
||||
pub material_states: [BlockStateId; 6],
|
||||
/// Block names in `#minecraft:blocks_fluid_flow` (fluid flow vectors).
|
||||
pub blocks_fluid_flow: BTreeSet<String>,
|
||||
}
|
||||
/// The block models and their baked sprites, read from the pack at `root` over
|
||||
/// an existing block registry: everything the mesher and renderer need.
|
||||
pub struct RenderAssets {
|
||||
pub sprite_size: u32,
|
||||
pub sprite_minimum_size: u32,
|
||||
pub pixels: Vec<u8>,
|
||||
@@ -96,10 +68,30 @@ pub struct RenderAssets {
|
||||
pub translucent_layers: Vec<bool>,
|
||||
/// Each layer's `MipmapGenerator` settings.
|
||||
pub sprite_mips: Vec<crate::mipmap::Settings>,
|
||||
/// Block names in `#minecraft:blocks_fluid_flow` (fluid flow vectors).
|
||||
pub blocks_fluid_flow: BTreeSet<String>,
|
||||
}
|
||||
fn invalid(message: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, message)
|
||||
}
|
||||
fn tag(value: &Value) -> Result<Tag> {
|
||||
Ok(match value {
|
||||
Value::String(value) => Tag::String(value.as_str().into()),
|
||||
Value::Number(value) => Tag::Int(i32::try_from(
|
||||
value
|
||||
.as_i64()
|
||||
.ok_or_else(|| invalid("invalid registry number"))?,
|
||||
)?),
|
||||
Value::Array(values) => Tag::List(values.iter().map(tag).collect::<Result<_>>()?),
|
||||
Value::Object(values) => Tag::Compound(
|
||||
values
|
||||
.iter()
|
||||
.map(|(name, value)| Ok((name.as_str().into(), tag(value)?)))
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
_ => return Err(invalid("unsupported registry value").into()),
|
||||
})
|
||||
}
|
||||
impl Assets {
|
||||
pub fn load(root: &Path) -> Result<Self> {
|
||||
let manifest: Value = serde_json::from_reader(BufReader::new(File::open(
|
||||
@@ -108,8 +100,23 @@ impl Assets {
|
||||
if manifest["data_version"].as_i64() != Some(i64::from(DATA_VERSION)) {
|
||||
return Err(invalid("resources do not match Horizon's pinned data version").into());
|
||||
}
|
||||
let (registry, flags) =
|
||||
BlockRegistry::from_facts_json_with_flags(File::open(root.join("facts.json"))?)?;
|
||||
let facts: Value = serde_json::from_slice(&fs::read(root.join("facts.json"))?)?;
|
||||
let states = facts["states"]
|
||||
.as_array()
|
||||
.ok_or_else(|| invalid("missing registry state facts"))?;
|
||||
let registry = BlockRegistry::from_nbt(&Tag::compound([
|
||||
("DataVersion", Tag::Int(DATA_VERSION)),
|
||||
(
|
||||
"states",
|
||||
Tag::List(
|
||||
states
|
||||
.iter()
|
||||
.map(|state| tag(&state[4]))
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
),
|
||||
("defaults", tag(&facts["defaults"])?),
|
||||
]))?;
|
||||
let bundle: Value =
|
||||
serde_json::from_reader(BufReader::new(File::open(root.join("block-tags.json"))?))?;
|
||||
let mut definitions = TagDefinitions::default();
|
||||
@@ -125,7 +132,7 @@ impl Assets {
|
||||
)?;
|
||||
}
|
||||
let built =
|
||||
definitions.build(|name, _| registry.default_state(name).map(|_| name.to_owned()))?;
|
||||
definitions.build(|name, _| facts["defaults"].get(name).map(|_| name.to_owned()))?;
|
||||
if !built.missing.is_empty() {
|
||||
return Err(invalid("unresolved block tags").into());
|
||||
}
|
||||
@@ -151,20 +158,27 @@ impl Assets {
|
||||
.cloned()
|
||||
.collect();
|
||||
let properties = Properties(Arc::new(
|
||||
flags
|
||||
states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &(air, fluid))| {
|
||||
let name = ®istry
|
||||
.state(BlockStateId(i as u32))
|
||||
.ok_or_else(|| invalid("missing registry state"))?
|
||||
.name;
|
||||
.map(|state| {
|
||||
Ok(StateFacts {
|
||||
air,
|
||||
fluid,
|
||||
air_block: name == "minecraft:air",
|
||||
motion: motion.contains(name.as_str()),
|
||||
no_leaves: no_leaves.contains(name.as_str()),
|
||||
air: state[1]
|
||||
.as_bool()
|
||||
.ok_or_else(|| invalid("missing air fact"))?,
|
||||
fluid: state[2]
|
||||
.as_bool()
|
||||
.ok_or_else(|| invalid("missing fluid fact"))?,
|
||||
air_block: state[0].as_str() == Some("minecraft:air"),
|
||||
motion: motion.contains(
|
||||
state[0]
|
||||
.as_str()
|
||||
.ok_or_else(|| invalid("missing state name"))?,
|
||||
),
|
||||
no_leaves: no_leaves.contains(
|
||||
state[0]
|
||||
.as_str()
|
||||
.ok_or_else(|| invalid("missing state name"))?,
|
||||
),
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
@@ -175,27 +189,34 @@ impl Assets {
|
||||
.default_state(&format!("minecraft:{name}"))
|
||||
.ok_or_else(|| invalid("missing test-scene material"))?;
|
||||
}
|
||||
// Chunks hold every state of this registry: content mods can take it
|
||||
// past vanilla's 16-bit global palette.
|
||||
crate::chunk::set_block_states(registry.len());
|
||||
let registry: &'static BlockRegistry = Box::leak(Box::new(registry));
|
||||
Ok(Self {
|
||||
Self::with_models(
|
||||
root,
|
||||
registry,
|
||||
properties,
|
||||
material_states,
|
||||
blocks_fluid_flow,
|
||||
})
|
||||
)
|
||||
}
|
||||
/// The models and sprites of another resource root over these blocks
|
||||
/// The same blocks with the models and sprites of another resource root
|
||||
/// (resource packs changed): the registry and block facts come from the
|
||||
/// game data, which resource packs do not touch.
|
||||
pub fn reload(&self, root: &Path) -> Result<RenderAssets> {
|
||||
RenderAssets::load(root, self)
|
||||
pub fn reload(&self, root: &Path) -> Result<Self> {
|
||||
Self::with_models(
|
||||
root,
|
||||
self.registry,
|
||||
self.properties.clone(),
|
||||
self.material_states,
|
||||
self.blocks_fluid_flow.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl RenderAssets {
|
||||
pub fn load(root: &Path, assets: &Assets) -> Result<Self> {
|
||||
let registry = assets.registry;
|
||||
fn with_models(
|
||||
root: &Path,
|
||||
registry: &'static BlockRegistry,
|
||||
properties: Properties,
|
||||
material_states: [BlockStateId; 6],
|
||||
blocks_fluid_flow: BTreeSet<String>,
|
||||
) -> Result<Self> {
|
||||
let mut sprites = crate::sprites::Sprites::default();
|
||||
for name in MATERIALS {
|
||||
sprites.insert(root, &format!("minecraft:block/{name}"))?;
|
||||
@@ -219,6 +240,9 @@ impl RenderAssets {
|
||||
}
|
||||
sprites.finish();
|
||||
Ok(Self {
|
||||
registry,
|
||||
properties,
|
||||
material_states,
|
||||
sprite_size: sprites.size,
|
||||
sprite_minimum_size: sprites.minimum_size,
|
||||
texture_layers: u32::try_from(sprites.count())?,
|
||||
@@ -231,6 +255,7 @@ impl RenderAssets {
|
||||
sprite_layers: sprites.layers,
|
||||
translucent_layers: sprites.translucent,
|
||||
sprite_mips: sprites.mips,
|
||||
blocks_fluid_flow,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,9 @@ impl AttackStrength {
|
||||
/// The attack-strength part of `Player.tick`: both tickers advance, and a
|
||||
/// main-hand change to a different item resets them
|
||||
/// (`!ItemStack.matches` and `!ItemStack.isSameItem`).
|
||||
/// Whether they were reset.
|
||||
pub fn tick(&mut self, main_hand: Option<HeldStack>) -> bool {
|
||||
pub fn tick(&mut self, main_hand: Option<HeldStack>) {
|
||||
self.ticker += 1;
|
||||
self.swap_ticker += 1;
|
||||
let mut reset = false;
|
||||
if self.last_item != main_hand {
|
||||
let same_item = match (&self.last_item, &main_hand) {
|
||||
(Some(a), Some(b)) => a.item == b.item,
|
||||
@@ -42,11 +40,9 @@ impl AttackStrength {
|
||||
};
|
||||
if !same_item {
|
||||
self.reset();
|
||||
reset = true;
|
||||
}
|
||||
self.last_item = main_hand;
|
||||
}
|
||||
reset
|
||||
}
|
||||
/// `resetAttackStrengthTicker`: swinging at nothing, attacking or
|
||||
/// stopping a block break.
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
//! small mixer standing in for OpenAL: one channel per source with its gain,
|
||||
//! pitch, linear distance attenuation (`AL_LINEAR_DISTANCE`, reference 0,
|
||||
//! rolloff 1), looping, relative flag and source state, Ogg/Vorbis decoding
|
||||
//! (streams on a worker thread) and OpenAL Soft's stereo panning.
|
||||
//! (streams on a worker thread) and stereo panning.
|
||||
use crate::Result;
|
||||
use crate::sound_engine::{
|
||||
Attenuation, Backend, Bound, ChannelId, InstanceId, PlayResult, SoundEngine, SoundInstance,
|
||||
Sounds, TickContext,
|
||||
};
|
||||
use crate::sound_events::{Sound, SoundRegistry};
|
||||
#[cfg(feature = "audio")]
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use horizon_core::random::{LegacyRandom, RandomSource};
|
||||
use std::{
|
||||
@@ -45,7 +44,6 @@ struct Mixer {
|
||||
channels: HashMap<ChannelId, Channel>,
|
||||
listener: [f64; 3],
|
||||
listener_yaw: f32,
|
||||
listener_pitch: f32,
|
||||
limiter_peak: f32,
|
||||
}
|
||||
/// A sound the player could have heard (`SoundEventListener.onPlaySound`):
|
||||
@@ -136,14 +134,8 @@ fn attach(mixer: &Mutex<Mixer>, channel: ChannelId, samples: Samples, looping: b
|
||||
c.looping |= looping;
|
||||
}
|
||||
}
|
||||
/// The open output stream. A build without the `audio` feature (the server build, which must not
|
||||
/// need libasound) never has one.
|
||||
#[cfg(feature = "audio")]
|
||||
type Stream = cpal::Stream;
|
||||
#[cfg(not(feature = "audio"))]
|
||||
type Stream = std::convert::Infallible;
|
||||
pub struct Audio {
|
||||
_stream: Option<Stream>,
|
||||
_stream: Option<cpal::Stream>,
|
||||
pub engine: SoundEngine,
|
||||
backend: MixerBackend,
|
||||
/// Record `Heard` sounds (the subtitle overlay is listening).
|
||||
@@ -158,9 +150,6 @@ pub struct Audio {
|
||||
/// Positional sounds played since the last drain, kept while the world
|
||||
/// is open to the network so guests hear them too.
|
||||
pub capture: Option<Vec<(String, [f64; 3], f32, f32)>>,
|
||||
/// How many times each positional sound played (the control socket's `status`),
|
||||
/// with or without an audio device.
|
||||
pub heard: std::collections::BTreeMap<String, u32>,
|
||||
}
|
||||
fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
|
||||
use symphonia::core::{
|
||||
@@ -335,32 +324,6 @@ impl Backend for MixerBackend {
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
/// OpenAL Soft's stereo gains `[left, right]` for a mono source in a
|
||||
/// direction given in listener space (right, up, forward; any length).
|
||||
/// Vanilla's context (`Library.createAttributes`: HRTF off) on a stereo
|
||||
/// output renders pairwise: front azimuths are stretched by 1.5
|
||||
/// (`ScaleAzimuthFront`, so 60 degrees off-centre pans fully) and the
|
||||
/// direction is decoded by the first-order stereo decoder, 0.5 W, 0.5 Y and
|
||||
/// 0.0957 X in this scaling. A centred source gets 0.596 on each side, one
|
||||
/// behind 0.404, one above or below 0.5, one at the source's own position
|
||||
/// is panned to the front. Matches OpenAL Soft 1.25.2 (LWJGL 3.4.3, the
|
||||
/// pinned client's) rendered through `ALC_SOFT_loopback` within 1e-5.
|
||||
fn stereo_gains(right: f32, up: f32, forward: f32) -> [f32; 2] {
|
||||
let length = (right * right + up * up + forward * forward).sqrt();
|
||||
if length <= f32::EPSILON {
|
||||
return stereo_gains(0.0, 0.0, 1.0);
|
||||
}
|
||||
let elevation = (up / length).clamp(-1.0, 1.0).asin();
|
||||
let mut azimuth = right.atan2(forward);
|
||||
if azimuth.abs() < std::f32::consts::FRAC_PI_2 {
|
||||
azimuth = (azimuth.abs() * 1.5)
|
||||
.min(std::f32::consts::FRAC_PI_2)
|
||||
.copysign(azimuth);
|
||||
}
|
||||
let side = azimuth.sin() * elevation.cos();
|
||||
let front = azimuth.cos() * elevation.cos() * 0.095_660_8;
|
||||
[0.5 - 0.5 * side + front, 0.5 + 0.5 * side + front]
|
||||
}
|
||||
/// The gain OpenAL applies for distance (`AL_LINEAR_DISTANCE` with reference
|
||||
/// distance 0 and rolloff 1: the distance clamps to the max distance).
|
||||
fn distance_gain(linear: Option<f32>, distance: f32) -> f32 {
|
||||
@@ -370,35 +333,12 @@ fn distance_gain(linear: Option<f32>, distance: f32) -> f32 {
|
||||
Some(_) => 0.0,
|
||||
}
|
||||
}
|
||||
// Only the output stream mixes; a build without one (no `audio` feature) never does.
|
||||
#[cfg_attr(not(feature = "audio"), allow(dead_code))]
|
||||
fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
data.fill(0.0);
|
||||
let listener = mixer.listener;
|
||||
// The camera's forward and up vectors (`Camera.forwardVector`/`upVector`)
|
||||
// and OpenAL's right = forward x up.
|
||||
let (yaw, pitch) = (
|
||||
f64::from(mixer.listener_yaw.to_radians()),
|
||||
f64::from(mixer.listener_pitch.to_radians()),
|
||||
);
|
||||
let forward = [
|
||||
-yaw.sin() * pitch.cos(),
|
||||
-pitch.sin(),
|
||||
yaw.cos() * pitch.cos(),
|
||||
];
|
||||
let up = [
|
||||
-yaw.sin() * pitch.sin(),
|
||||
pitch.cos(),
|
||||
yaw.cos() * pitch.sin(),
|
||||
];
|
||||
let right = [
|
||||
forward[1] * up[2] - forward[2] * up[1],
|
||||
forward[2] * up[0] - forward[0] * up[2],
|
||||
forward[0] * up[1] - forward[1] * up[0],
|
||||
];
|
||||
let dot = |a: [f64; 3], b: [f64; 3]| (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) as f32;
|
||||
let yaw = mixer.listener_yaw.to_radians();
|
||||
// Gains and pans per source for this buffer.
|
||||
let mut voices: Vec<(ChannelId, [f32; 3], f64)> = Vec::new();
|
||||
let mut voices: Vec<(ChannelId, f32, f32, f64)> = Vec::new();
|
||||
for (id, c) in &mixer.channels {
|
||||
let Some((_, source_rate)) = &c.samples else {
|
||||
continue;
|
||||
@@ -417,17 +357,19 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
};
|
||||
let distance = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() as f32;
|
||||
let gain = c.gain.clamp(0.0, 1.0) * distance_gain(c.linear, distance);
|
||||
// Relative sources are already in OpenAL's listener space (-Z ahead).
|
||||
let [left, right] = if c.relative {
|
||||
stereo_gains(d[0] as f32, d[1] as f32, -d[2] as f32)
|
||||
// The listener's right vector for a Minecraft yaw.
|
||||
let side = if c.relative {
|
||||
(d[0] as f32) / distance.max(0.001)
|
||||
} else {
|
||||
stereo_gains(dot(d, right), dot(d, up), dot(d, forward))
|
||||
}
|
||||
.map(|pan| gain * pan);
|
||||
(-(yaw.cos()) as f64 * d[0] - (yaw.sin()) as f64 * d[2]) as f32 / distance.max(0.001)
|
||||
};
|
||||
let pan = side.clamp(-1.0, 1.0);
|
||||
let left = gain * (1.0 - pan.max(0.0) * 0.6);
|
||||
let right = gain * (1.0 + pan.min(0.0) * 0.6);
|
||||
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
|
||||
voices.push((*id, [left, right, gain], step));
|
||||
voices.push((*id, left, right, step));
|
||||
}
|
||||
for (id, [left, right, gain], step) in voices {
|
||||
for (id, left, right, step) in voices {
|
||||
let Some(c) = mixer.channels.get_mut(&id) else {
|
||||
continue;
|
||||
};
|
||||
@@ -451,19 +393,16 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
frame[0] += s * left;
|
||||
frame[1] += s * right;
|
||||
} else {
|
||||
// A mono output has no panning: the source's own gain.
|
||||
frame[0] += s * gain;
|
||||
frame[0] += s * (left + right) / 2.0;
|
||||
}
|
||||
c.position += step;
|
||||
}
|
||||
}
|
||||
// Limit the whole buffer together, preserving waveform and stereo balance
|
||||
// instead of flattening each overloaded sample; release over 200 ms
|
||||
// (OpenAL Soft's output limiter, `ALC_OUTPUT_LIMITER_SOFT`, releases
|
||||
// over 0.2 s).
|
||||
// instead of flattening each overloaded sample; release over 50 ms.
|
||||
let peak = data.iter().fold(1.0f32, |peak, s| peak.max(s.abs()));
|
||||
mixer.limiter_peak = mixer.limiter_peak.max(peak);
|
||||
let release = (-1.0 / (0.2 * rate as f32)).exp();
|
||||
let release = (-1.0 / (0.05 * rate as f32)).exp();
|
||||
for frame in data.chunks_mut(channels) {
|
||||
let gain = mixer.limiter_peak.recip();
|
||||
for s in frame {
|
||||
@@ -476,7 +415,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
/// `HORIZON_NO_AUDIO`, or a virtual screen (`vscreen`), where agents and
|
||||
/// harnesses run the game and must stay off the user's speakers.
|
||||
/// `HORIZON_AUDIO=1` opens the output anyway (testing into a null sink).
|
||||
#[cfg(any(feature = "audio", test))]
|
||||
fn output_disabled(var: impl Fn(&str) -> Option<std::ffi::OsString>) -> Option<String> {
|
||||
if var("HORIZON_NO_AUDIO").is_some() {
|
||||
return Some("audio off: HORIZON_NO_AUDIO is set".into());
|
||||
@@ -492,7 +430,6 @@ fn output_disabled(var: impl Fn(&str) -> Option<std::ffi::OsString>) -> Option<S
|
||||
}
|
||||
/// An output stream on `device` in its default configuration, whatever its
|
||||
/// sample format: the mixer works in f32 and converts.
|
||||
#[cfg(feature = "audio")]
|
||||
fn open_device(
|
||||
device: &cpal::Device,
|
||||
mixer: Arc<Mutex<Mixer>>,
|
||||
@@ -513,7 +450,6 @@ fn open_device(
|
||||
stream.play()?;
|
||||
Ok((stream, config))
|
||||
}
|
||||
#[cfg(feature = "audio")]
|
||||
fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
|
||||
device: &cpal::Device,
|
||||
config: &cpal::SupportedStreamConfig,
|
||||
@@ -550,15 +486,6 @@ impl Audio {
|
||||
}
|
||||
fn with_output(root: &Path, output: bool) -> Self {
|
||||
let registry = SoundRegistry::load(root);
|
||||
if output && registry.is_empty() {
|
||||
// The game then plays nothing on a working output (#1035, #1076).
|
||||
log::warn!(
|
||||
"Horizon: no sounds in {}: they were not copied from your Minecraft \
|
||||
install; prepare the game again in the launcher (or run \
|
||||
horizon-prepare sounds)",
|
||||
root.join("assets").display()
|
||||
);
|
||||
}
|
||||
let mixer = Arc::new(Mutex::new(Mixer::default()));
|
||||
let opened = if output {
|
||||
Self::open(mixer.clone())
|
||||
@@ -594,15 +521,9 @@ impl Audio {
|
||||
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
|
||||
source_override: None,
|
||||
capture: None,
|
||||
heard: Default::default(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "audio"))]
|
||||
fn open(_: Arc<Mutex<Mixer>>) -> Result<Stream> {
|
||||
Err("this build has no sound output (built without the `audio` feature)".into())
|
||||
}
|
||||
#[cfg(feature = "audio")]
|
||||
fn open(mixer: Arc<Mutex<Mixer>>) -> Result<Stream> {
|
||||
fn open(mixer: Arc<Mutex<Mixer>>) -> Result<cpal::Stream> {
|
||||
if let Some(reason) = output_disabled(|name| std::env::var_os(name)) {
|
||||
return Err(reason.into());
|
||||
}
|
||||
@@ -644,15 +565,13 @@ impl Audio {
|
||||
backend: &mut self.backend,
|
||||
}
|
||||
}
|
||||
/// `SoundManager.updateSource`: the camera position and rotation
|
||||
/// (`Listener.setTransform` takes its forward and up vectors).
|
||||
pub fn set_listener(&mut self, position: [f64; 3], yaw: f32, pitch: f32) {
|
||||
/// `SoundManager.updateSource`: the camera position and yaw.
|
||||
pub fn set_listener(&mut self, position: [f64; 3], yaw: f32) {
|
||||
self.listener = position;
|
||||
self.listener_yaw = yaw;
|
||||
if let Ok(mut m) = self.backend.mixer.lock() {
|
||||
m.listener = position;
|
||||
m.listener_yaw = yaw;
|
||||
m.listener_pitch = pitch;
|
||||
}
|
||||
}
|
||||
/// Sync the volume options, refreshing the categories that changed
|
||||
@@ -737,9 +656,6 @@ impl Audio {
|
||||
if let (Some(capture), Some(at)) = (self.capture.as_mut(), at) {
|
||||
capture.push((event.to_owned(), at, volume, pitch));
|
||||
}
|
||||
if at.is_some() {
|
||||
*self.heard.entry(event.to_owned()).or_default() += 1;
|
||||
}
|
||||
self.sync_volumes();
|
||||
let event = crate::sound_events::normalize(event);
|
||||
let source = self
|
||||
@@ -966,9 +882,8 @@ mod tests {
|
||||
}
|
||||
let mut data = [0.0; 16];
|
||||
mix(&mut m, &mut data, 2, 48_000);
|
||||
// West of a listener facing south: fully on the right.
|
||||
for (frame, sample) in data.as_chunks::<2>().0.iter().zip(wave) {
|
||||
assert!(frame[0].abs() < 1e-6, "{frame:?}");
|
||||
assert!((frame[0] - sample * 0.4).abs() < 1e-6, "{frame:?}");
|
||||
assert!((frame[1] - sample).abs() < 1e-6, "{frame:?}");
|
||||
}
|
||||
assert!(data.iter().all(|s| s.abs() <= 1.0));
|
||||
@@ -979,10 +894,9 @@ mod tests {
|
||||
let mut m = Mixer::default();
|
||||
m.channels
|
||||
.insert(1, channel(vec![0.25; 2], None, [0.0; 3], true));
|
||||
let quiet = 0.25 * stereo_gains(0.0, 0.0, 1.0)[0];
|
||||
let mut data = [0.0; 16];
|
||||
mix(&mut m, &mut data, 2, 48_000);
|
||||
assert_eq!(data, [quiet; 16]);
|
||||
assert_eq!(data, [0.25; 16]);
|
||||
|
||||
for id in 2..=5 {
|
||||
m.channels
|
||||
@@ -995,7 +909,7 @@ mod tests {
|
||||
mix(&mut m, &mut recovery, 2, 48_000);
|
||||
assert!(recovery[0] > 0.0 && recovery[0] < 0.1);
|
||||
assert!(recovery.windows(2).all(|w| w[1] >= w[0]));
|
||||
assert_eq!(*recovery.last().unwrap(), quiet);
|
||||
assert_eq!(*recovery.last().unwrap(), 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1013,60 +927,4 @@ mod tests {
|
||||
let first = data[0] + data[1];
|
||||
assert!(first > 1.4 && first < 3.1, "{first}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stereo_panning_matches_openal_soft() {
|
||||
// OpenAL Soft 1.25.2 (LWJGL 3.4.3's macOS build), stereo loopback
|
||||
// device with vanilla's attributes, 1 kHz sine 4 blocks away.
|
||||
let cases = [
|
||||
((0.0, 0.0, 1.0), [0.5957, 0.5957]),
|
||||
((-1.0, 0.0, 0.0), [1.0, 0.0]),
|
||||
((1.0, 0.0, 0.0), [0.0, 1.0]),
|
||||
((0.0, 0.0, -1.0), [0.4043, 0.4043]),
|
||||
((0.0, 1.0, 0.0), [0.5, 0.5]),
|
||||
((0.0, -1.0, 0.0), [0.5, 0.5]),
|
||||
((-1.0, 0.0, 1.0), [0.9985, 0.0747]),
|
||||
((-1.0, 0.0, -1.0), [0.7859, 0.0788]),
|
||||
((0.0, 0.0, 0.0), [0.5957, 0.5957]),
|
||||
];
|
||||
for ((right, up, forward), expected) in cases {
|
||||
let gains = stereo_gains(right, up, forward);
|
||||
for (g, e) in gains.into_iter().zip(expected) {
|
||||
assert!((g - e).abs() < 1e-4, "{right} {up} {forward}: {gains:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panning_follows_the_camera_rotation() {
|
||||
let gains = |yaw: f32, pitch: f32, at: [f32; 3]| {
|
||||
let mut m = Mixer {
|
||||
listener_yaw: yaw,
|
||||
listener_pitch: pitch,
|
||||
..Mixer::default()
|
||||
};
|
||||
m.channels.insert(1, channel(vec![1.0; 4], None, at, true));
|
||||
let mut data = [0.0; 2];
|
||||
mix(&mut m, &mut data, 2, 48_000);
|
||||
data
|
||||
};
|
||||
let close =
|
||||
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1e-4 && (a[1] - b[1]).abs() < 1e-4;
|
||||
// Facing south (yaw 0), east is on the left; facing north, on the right.
|
||||
assert!(close(gains(0.0, 0.0, [1.0, 0.0, 0.0]), [1.0, 0.0]));
|
||||
assert!(close(gains(180.0, 0.0, [1.0, 0.0, 0.0]), [0.0, 1.0]));
|
||||
// Looking straight down, a source below is ahead and one south is above.
|
||||
assert!(close(gains(0.0, 90.0, [0.0, -1.0, 0.0]), [0.5957, 0.5957]));
|
||||
assert!(close(gains(0.0, 90.0, [0.0, 0.0, 1.0]), [0.5, 0.5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mono_output_plays_sources_unpanned() {
|
||||
let mut m = Mixer::default();
|
||||
m.channels
|
||||
.insert(1, channel(vec![0.5; 4], None, [0.0, 0.0, -3.0], true));
|
||||
let mut data = [0.0; 2];
|
||||
mix(&mut m, &mut data, 1, 48_000);
|
||||
assert_eq!(data, [0.5; 2]);
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ pub fn discovery_url() -> String {
|
||||
fn agent() -> ureq::Agent {
|
||||
ureq::AgentBuilder::new()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.user_agent(&format!("Horizon/{}", horizon_core::release::VERSION))
|
||||
.user_agent(concat!("Horizon/", env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -140,28 +140,11 @@ fn run(control: &Control, setup: Setup) -> Result<PathBuf, String> {
|
||||
driver: text(&renderer["driver"]),
|
||||
backend: text(&renderer["backend"]).to_lowercase(),
|
||||
kind: text(&renderer["device_type"]),
|
||||
vram_mb: None,
|
||||
});
|
||||
if let Some(gpu) = report.hardware.gpu.as_mut() {
|
||||
let vendor = horizon_telemetry::system::gpu_vendor(&gpu.name, 0);
|
||||
let vendor = if gpu.vendor.is_empty() {
|
||||
vendor
|
||||
} else {
|
||||
gpu.vendor.as_str()
|
||||
}
|
||||
.to_owned();
|
||||
if gpu.kind.contains("iscrete") {
|
||||
gpu.vram_mb = horizon_telemetry::system::vram_mb(&vendor);
|
||||
}
|
||||
}
|
||||
report.hardware.storage =
|
||||
horizon_telemetry::system::storage(&setup.game_dir).map(str::to_owned);
|
||||
report.hardware.display = serde_json::from_value(title["display"].clone()).ok();
|
||||
|
||||
replace_world(&setup.game_dir)?;
|
||||
control.call(&format!("open {WORLD} {SEED} creative"))?;
|
||||
// The world opens on a worker: its folder may not exist yet when `open` replies.
|
||||
std::fs::create_dir_all(setup.game_dir.join("saves").join(WORLD)).map_err(|e| e.to_string())?;
|
||||
std::fs::write(
|
||||
setup.game_dir.join("saves").join(WORLD).join(MARKER),
|
||||
"Created by horizon-client --benchmark; replaced by the next run.\n",
|
||||
@@ -239,10 +222,6 @@ fn run(control: &Control, setup: Setup) -> Result<PathBuf, String> {
|
||||
|
||||
let status = control.status();
|
||||
let renderer = &status["renderer"];
|
||||
// On Wayland the window is on a screen only once it has been shown: ask again.
|
||||
if report.hardware.display.is_none() {
|
||||
report.hardware.display = serde_json::from_value(status["display"].clone()).ok();
|
||||
}
|
||||
let c = &mut report.conditions;
|
||||
c.scenario = Some(setup.scenario.clone());
|
||||
c.seed = Some(SEED.to_string());
|
||||
|
||||
@@ -32,18 +32,16 @@ pub enum Group {
|
||||
Move,
|
||||
Act,
|
||||
Screens,
|
||||
Multiplayer,
|
||||
World,
|
||||
Scripting,
|
||||
}
|
||||
impl Group {
|
||||
pub const ALL: [Group; 8] = [
|
||||
pub const ALL: [Group; 7] = [
|
||||
Group::Session,
|
||||
Group::Observe,
|
||||
Group::Move,
|
||||
Group::Act,
|
||||
Group::Screens,
|
||||
Group::Multiplayer,
|
||||
Group::World,
|
||||
Group::Scripting,
|
||||
];
|
||||
@@ -54,7 +52,6 @@ impl Group {
|
||||
Group::Move => "Move and look",
|
||||
Group::Act => "Act on the world",
|
||||
Group::Screens => "Screens, menus and chat",
|
||||
Group::Multiplayer => "Multiplayer, LAN and friends",
|
||||
Group::World => "World and cheats",
|
||||
Group::Scripting => "Scripting and benchmarks",
|
||||
}
|
||||
@@ -120,50 +117,6 @@ const OVER: Param = flag(
|
||||
"turn with mouse motion spread over this many seconds (eased), as a player would",
|
||||
);
|
||||
const FACE: Ty = Ty::Enum(&["down", "up", "north", "south", "west", "east"]);
|
||||
const BOX: [Param; 6] = [
|
||||
p("x1", Ty::Int, "corner X"),
|
||||
p("y1", Ty::Int, "corner Y"),
|
||||
p("z1", Ty::Int, "corner Z"),
|
||||
p("x2", Ty::Int, "opposite X"),
|
||||
p("y2", Ty::Int, "opposite Y"),
|
||||
p("z2", Ty::Int, "opposite Z"),
|
||||
];
|
||||
const PAGES: Ty = Ty::Enum(&[
|
||||
"main",
|
||||
"video",
|
||||
"sound",
|
||||
"controls",
|
||||
"keybinds",
|
||||
"controller",
|
||||
"controller-binds",
|
||||
"mouse",
|
||||
"chat",
|
||||
"accessibility",
|
||||
"language",
|
||||
"font",
|
||||
"packs",
|
||||
"mods",
|
||||
"graphics",
|
||||
"graphics-advanced",
|
||||
"extras",
|
||||
"horizon",
|
||||
"shader-packs",
|
||||
"agent",
|
||||
]);
|
||||
|
||||
/// `--confirm`, required by destructive commands: the player agreed.
|
||||
const CONFIRM: Param = Param {
|
||||
name: "confirm",
|
||||
ty: Ty::Bool,
|
||||
required: true,
|
||||
flag: true,
|
||||
help: "true once the player agreed: it cannot be undone",
|
||||
};
|
||||
|
||||
/// MCP tools that delete or overwrite the player's things (`destructiveHint`).
|
||||
pub const DESTRUCTIVE: &[&str] = &["world-delete", "fill", "setblock"];
|
||||
/// MCP tools that reach other players or the network (`openWorldHint`): only when the player asks.
|
||||
pub const OPEN_WORLD: &[&str] = &["connect", "lan", "friends", "chat", "command"];
|
||||
|
||||
macro_rules! cmd {
|
||||
($name:expr, $group:ident, $usage:expr, $summary:expr, $details:expr, [$($ex:expr),* $(,)?], $params:expr, mcp: $mcp:expr, local: $local:expr $(, wire: $wire:expr)?) => {
|
||||
@@ -211,19 +164,7 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
&[flag("lines", Ty::Int, "how many lines of latest.log (default 100)"), flag("crash", Ty::Bool, "the newest crash report instead")],
|
||||
mcp: true, local: true),
|
||||
cmd!("status", Session, "status", "game, renderer and streaming state (the scripting socket's original summary)",
|
||||
"Also what F3 shows: the overlay flags, the renderer, chunk streaming and memory.", ["horizon-ctl status"], &[], mcp: true, local: false),
|
||||
cmd!("worlds", Session, "worlds", "the saved worlds, newest first: folder, name, mode, last played, version",
|
||||
"folder is what open, world-rename, world-backup and world-delete take.", ["horizon-ctl worlds"], &[], mcp: true, local: false),
|
||||
cmd!("world-rename", Session, "world-rename FOLDER NAME", "rename a world, as Edit World does (its folder stays)",
|
||||
"Needs the title screen (no world open).", ["horizon-ctl world-rename Test \"My Base\""],
|
||||
&[p("folder", Ty::Str, "world folder from worlds"), p("name", Ty::Rest, "the new name")], mcp: true, local: false),
|
||||
cmd!("world-backup", Session, "world-backup FOLDER", "zip a world into backups/, as Edit World's Make Backup does",
|
||||
"Needs the title screen. Replies once the zip is written, with its size.", ["horizon-ctl world-backup Test"],
|
||||
&[p("folder", Ty::Str, "world folder from worlds")], mcp: true, local: false),
|
||||
cmd!("world-delete", Session, "world-delete FOLDER --confirm", "delete a world for good, as Select World's Delete does",
|
||||
"Needs the title screen and --confirm: ask the player first, it cannot be undone (world-backup keeps a copy).",
|
||||
["horizon-ctl world-delete Test --confirm"],
|
||||
&[p("folder", Ty::Str, "world folder from worlds"), CONFIRM], mcp: true, local: false),
|
||||
"", ["horizon-ctl status"], &[], mcp: false, local: false),
|
||||
cmd!("open", Session, "open WORLD [SEED] [creative]", "open or create a world from the title screen",
|
||||
"Opens saves/WORLD, creating it with SEED (and creative mode) when it doesn't exist.",
|
||||
["horizon-ctl open Test 12345", "horizon-ctl open Build 0 creative"],
|
||||
@@ -246,13 +187,9 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
&[opt("radius", Ty::Num, "search radius in blocks (default 32)"), opt("type", Ty::Str, "entity type filter")], mcp: true, local: false),
|
||||
cmd!("block", Observe, "block X Y Z", "the block state at a position", "",
|
||||
["horizon-ctl block 10 64 -3"], &BLOCK_XYZ, mcp: true, local: false),
|
||||
cmd!("light", Observe, "light X1 Y1 Z1 X2 Y2 Z2", "the stored sky and block light of a box",
|
||||
"At most 32768 blocks per request. One hex digit per block in `sky` and `block`, x fastest, then z, then y; `.` where no chunk is loaded.",
|
||||
["horizon-ctl light 0 64 0 15 70 15"],
|
||||
&BOX, mcp: true, local: false),
|
||||
cmd!("blocks", Observe, "blocks X1 Y1 Z1 X2 Y2 Z2", "the non-air blocks of a box, with counts per block",
|
||||
"At most 32768 blocks per request.", ["horizon-ctl blocks 0 60 0 15 70 15"],
|
||||
&BOX, mcp: true, local: false),
|
||||
&[p("x1", Ty::Int, "corner X"), p("y1", Ty::Int, "corner Y"), p("z1", Ty::Int, "corner Z"), p("x2", Ty::Int, "opposite X"), p("y2", Ty::Int, "opposite Y"), p("z2", Ty::Int, "opposite Z")], mcp: true, local: false),
|
||||
cmd!("find-block", Observe, "find-block BLOCK [RADIUS]", "the nearest loaded blocks of a kind",
|
||||
"Searches a cube of RADIUS (default 32, at most 64) around the player; returns the count and the 16 nearest.",
|
||||
["horizon-ctl find-block oak_log", "horizon-ctl find-block diamond_ore 48"],
|
||||
@@ -323,15 +260,12 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
"down holds the key until key ... up, release or stop; wait does not release it. move, goto, jump, break and the other actions release the movement keys when they end.",
|
||||
["horizon-ctl key space tap"], &[p("key", Ty::Str, "key name"), p("action", Ty::Enum(&["down", "up", "tap", "repeat"]), "what to do")], mcp: true, local: false),
|
||||
cmd!("mouse", Act, "mouse left|right|middle down|up|click", "press or release a mouse button in the world", "",
|
||||
["horizon-ctl mouse right click"], &[p("button", Ty::Enum(&["left", "right", "middle"]), "button"), p("action", Ty::Enum(&["down", "up", "click"]), "what to do")], mcp: true, local: false),
|
||||
cmd!("interact-entity", Act, "interact-entity ID", "right-click an entity: trade with a villager, mount a horse, ride a boat, shear a sheep",
|
||||
"Faces the entity (from entities), right-clicks it with the held item, waits two ticks and replies with the screen.",
|
||||
["horizon-ctl interact-entity 42"], &[p("id", Ty::Int, "entity id from entities")], mcp: true, local: true),
|
||||
["horizon-ctl mouse right click"], &[p("button", Ty::Enum(&["left", "right", "middle"]), "button"), p("action", Ty::Enum(&["down", "up", "click"]), "what to do")], mcp: false, local: false),
|
||||
cmd!("pad", Act, "pad BUTTON down|up|tap | pad stick left|right X Y | pad trigger left|right V | pad release | pad status", "a virtual controller: buttons, sticks and triggers as a real pad would send them",
|
||||
"Buttons by position: south east west north lb rb lt rt select start ls rs up down left right (south is Xbox A / PlayStation cross). Sticks run -1..1 with up positive and stay tilted until centred; triggers 0..1. Merged with any real controller; status lists the connected ones and what is held.",
|
||||
["horizon-ctl pad south tap", "horizon-ctl pad stick left 0 1", "horizon-ctl pad trigger right 1", "horizon-ctl pad status"],
|
||||
&[p("what", Ty::Str, "a button name, stick, trigger, release or status"), opt("a", Ty::Str, "down|up|tap, or left|right"), opt("b", Ty::Num, "stick X or trigger value"), opt("c", Ty::Num, "stick Y")], mcp: true, local: false),
|
||||
cmd!("release", Act, "release", "release every key and mouse button", "", ["horizon-ctl release"], &[], mcp: true, local: false),
|
||||
&[p("what", Ty::Str, "a button name, stick, trigger, release or status"), opt("a", Ty::Str, "down|up|tap, or left|right"), opt("b", Ty::Num, "stick X or trigger value"), opt("c", Ty::Num, "stick Y")], mcp: false, local: false),
|
||||
cmd!("release", Act, "release", "release every key and mouse button", "", ["horizon-ctl release"], &[], mcp: false, local: false),
|
||||
cmd!("stop", Act, "stop", "cancel the running action (goto, break...) and release every key", "", ["horizon-ctl stop"], &[], mcp: true, local: false),
|
||||
// Screens
|
||||
cmd!("open-inventory", Screens, "open-inventory", "open the inventory screen (crafting grid 100-103, result 200)", "",
|
||||
@@ -344,10 +278,10 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
cmd!("ui", Screens, "ui", "every widget of the open screen: id, kind, label, bounds, value",
|
||||
"Kinds: button, slider (value 0..1), field, row (worlds, servers, packs, languages), tab (selected), slot (slot-ID, with its stack). Ids come from labels (\"FOV: 70\" is fov, a repeat gets -2) or kind-N without one, and stay put when a value changes. Reflects the last drawn frame (frame counts them).",
|
||||
["horizon-ctl ui"], &[], mcp: true, local: false),
|
||||
cmd!("click", Screens, "click ID [right|middle] [shift]", "click a widget of ui by its id, as the mouse would",
|
||||
"A field takes focus (then type); slot-ID clicks a slot like click-slot. middle presses the middle button (pick block: a creative player clones the stack).",
|
||||
cmd!("click", Screens, "click ID [right] [shift]", "click a widget of ui by its id, as the mouse would",
|
||||
"A field takes focus (then type); slot-ID clicks a slot like click-slot.",
|
||||
["horizon-ctl click done", "horizon-ctl click video-settings", "horizon-ctl click row-0", "horizon-ctl click slot-0 shift"],
|
||||
&[p("id", Ty::Str, "widget id from ui"), opt("right", Ty::Bool, "right-click"), opt("middle", Ty::Bool, "middle-click"), opt("shift", Ty::Bool, "shift-click (slots)")], mcp: true, local: false),
|
||||
&[p("id", Ty::Str, "widget id from ui"), opt("right", Ty::Bool, "right-click"), opt("shift", Ty::Bool, "shift-click (slots)")], mcp: true, local: false),
|
||||
cmd!("set", Screens, "set ID VALUE", "move a slider of ui to VALUE (0..1), as a click on its track",
|
||||
"The option behind it rounds to its own steps; read the new label with ui.",
|
||||
["horizon-ctl set fov 0.5", "horizon-ctl set render-distance 0"],
|
||||
@@ -379,31 +313,7 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
cmd!("respawn", Screens, "respawn", "respawn from the death screen", "", ["horizon-ctl respawn"], &[], mcp: true, local: false),
|
||||
cmd!("pause", Screens, "pause", "open the pause menu", "", ["horizon-ctl pause"], &[], mcp: true, local: false),
|
||||
cmd!("resume", Screens, "resume", "leave the pause menu", "", ["horizon-ctl resume"], &[], mcp: true, local: false),
|
||||
cmd!("options", Screens, "options [PAGE]", "open the options screen, or one of its pages (Horizon's too)",
|
||||
"Then ui lists its widgets and click, set and type change them, as with the mouse. options-set changes an options.txt value without the screen.",
|
||||
["horizon-ctl options video", "horizon-ctl options horizon"], &[opt("page", PAGES, "the page (default: the main options)")], mcp: true, local: false),
|
||||
cmd!("book", Screens, "book [next|back|take|done]", "the open book or lectern: its page text, or press one of its buttons",
|
||||
"", ["horizon-ctl book", "horizon-ctl book next"], &[opt("action", Ty::Enum(&["next", "back", "take", "done"]), "a button to press")], mcp: true, local: false),
|
||||
cmd!("signedit", Screens, "signedit X Y Z [front|back]", "open the sign editor of the sign at X Y Z (then type and close)",
|
||||
"", ["horizon-ctl signedit 3 64 1"], &[BLOCK_XYZ[0], BLOCK_XYZ[1], BLOCK_XYZ[2], opt("side", Ty::Enum(&["front", "back"]), "which side")], mcp: true, local: false),
|
||||
cmd!("mount-menu", Screens, "mount-menu", "open the inventory of the horse, donkey, mule or llama being ridden",
|
||||
"As the inventory key does while riding; also opens the nearest one's.", ["horizon-ctl mount-menu"], &[], mcp: true, local: false),
|
||||
cmd!("chat-log", Screens, "chat-log [N]", "the newest N chat messages", "", ["horizon-ctl chat-log 5"], &[], mcp: false, local: false),
|
||||
// Multiplayer
|
||||
cmd!("connect", Multiplayer, "connect ADDRESS [NAME]", "join a multiplayer server, or a friend's world by its invite code, from the title screen",
|
||||
"ADDRESS is HOST[:PORT], or friend:CODE for an invite code. Only when the player asks: it reaches the network.",
|
||||
["horizon-ctl connect play.example.net", "horizon-ctl connect localhost:25565", "horizon-ctl connect friend:0B33K-Q405Q-..."],
|
||||
&[p("address", Ty::Str, "HOST[:PORT] or friend:CODE"), opt("name", Ty::Str, "player name when offline")], mcp: true, local: false),
|
||||
cmd!("disconnect", Multiplayer, "disconnect", "leave the server, back to the server list", "", ["horizon-ctl disconnect"], &[], mcp: true, local: false),
|
||||
cmd!("servers", Multiplayer, "servers", "the Multiplayer screen's server list: name and address",
|
||||
"Add, edit and remove them on the Multiplayer screen (click-button Multiplayer, then ui).", ["horizon-ctl servers"], &[], mcp: true, local: false),
|
||||
cmd!("lan", Multiplayer, "lan open [PORT] [commands] [force] | lan close | lan status", "open the world to LAN (Open to LAN), stop, or see the guests",
|
||||
"PORT 0 or none picks one; commands lets guests use cheats; force forces the game mode. Only when the player asks.",
|
||||
["horizon-ctl lan open", "horizon-ctl lan status", "horizon-ctl lan close"],
|
||||
&[p("action", Ty::Enum(&["open", "close", "status"]), "what to do"), opt("port", Ty::Int, "port for open"), opt("commands", Ty::Bool, "allow cheats"), opt("force", Ty::Bool, "force the game mode")], mcp: true, local: false),
|
||||
cmd!("friends", Multiplayer, "friends host|stop|status", "Play with Friends: share the open world behind an invite code, stop, or see who joined",
|
||||
"status gives the code: share it only when the player asks. Friends join with connect friend:CODE.",
|
||||
["horizon-ctl friends host", "horizon-ctl friends status"], &[p("action", Ty::Enum(&["host", "stop", "status"]), "what to do")], mcp: true, local: false),
|
||||
// World
|
||||
cmd!("give", World, "give ITEM [COUNT]", "add items to the inventory", "", ["horizon-ctl give oak_log 16"],
|
||||
&[p("item", Ty::Str, "item id"), opt("count", Ty::Int, "how many (default 1)")], mcp: true, local: false),
|
||||
@@ -411,20 +321,8 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
&[p("ticks", Ty::Int, "day time")], mcp: true, local: false),
|
||||
cmd!("setblock", World, "setblock X Y Z BLOCK[props]", "set a block (with neighbour updates)", "", ["horizon-ctl setblock 0 64 0 stone"],
|
||||
&[BLOCK_XYZ[0], BLOCK_XYZ[1], BLOCK_XYZ[2], p("block", Ty::Str, "block state such as oak_stairs[facing=east]")], mcp: true, local: false),
|
||||
cmd!("summon", World, "summon KIND [X Y Z] [baby]", "spawn a mob (in front of the player without a position)", "Replies with its id.", ["horizon-ctl summon cow", "horizon-ctl summon pig 10 64 3 baby"],
|
||||
&[p("kind", Ty::Str, "mob type"), opt("x", Ty::Num, "X"), opt("y", Ty::Num, "Y"), opt("z", Ty::Num, "Z"), opt("baby", Ty::Bool, "a baby")], mcp: true, local: false),
|
||||
cmd!("effect", World, "effect ID SECONDS [AMPLIFIER] | effect clear", "give a status effect, or clear them all", "", ["horizon-ctl effect speed 30 1", "horizon-ctl effect clear"],
|
||||
&[p("id", Ty::Str, "effect id such as speed, or clear"), opt("seconds", Ty::Num, "duration"), opt("amplifier", Ty::Int, "level - 1 (default 0)")], mcp: true, local: false),
|
||||
cmd!("effects", World, "effects", "the player's active status effects", "", ["horizon-ctl effects"], &[], mcp: true, local: false),
|
||||
cmd!("xp", World, "xp LEVELS", "add experience levels (negative removes)", "", ["horizon-ctl xp 30"], &[p("levels", Ty::Int, "levels to add")], mcp: true, local: false),
|
||||
cmd!("enchant", World, "enchant ID LEVEL", "enchant the held item (a book stores it)", "", ["horizon-ctl enchant sharpness 5"],
|
||||
&[p("id", Ty::Str, "enchantment id"), p("level", Ty::Int, "level")], mcp: true, local: false),
|
||||
cmd!("potion", World, "potion ITEM POTION", "give a potion item", "", ["horizon-ctl potion splash_potion healing"],
|
||||
&[p("item", Ty::Enum(&["potion", "splash_potion", "lingering_potion", "tipped_arrow"]), "which item"), p("potion", Ty::Str, "potion id")], mcp: true, local: false),
|
||||
cmd!("fill", World, "fill X1 Y1 Z1 X2 Y2 Z2 BLOCK", "set a box of loaded blocks", "", ["horizon-ctl fill 0 64 0 4 64 4 glass"],
|
||||
&[BOX[0], BOX[1], BOX[2], BOX[3], BOX[4], BOX[5], p("block", Ty::Str, "block state")], mcp: true, local: false),
|
||||
cmd!("container", World, "container X Y Z [ITEM COUNT]", "list a container's items, or add items to it", "", ["horizon-ctl container 3 64 1", "horizon-ctl container 3 64 1 diamond 3"],
|
||||
&[BLOCK_XYZ[0], BLOCK_XYZ[1], BLOCK_XYZ[2], opt("item", Ty::Str, "item to add"), opt("count", Ty::Int, "how many")], mcp: true, local: false),
|
||||
cmd!("summon", World, "summon KIND [X Y Z] [baby]", "spawn a mob", "", ["horizon-ctl summon cow"], &[p("kind", Ty::Str, "mob type")], mcp: true, local: false),
|
||||
cmd!("effect", World, "effect ID SECONDS [AMPLIFIER] | effect clear", "give a status effect", "", ["horizon-ctl effect speed 30 1"], &[], mcp: false, local: false),
|
||||
cmd!("render-distance", World, "render-distance N", "chunks, 2..64", "", ["horizon-ctl render-distance 8"], &[p("chunks", Ty::Int, "render distance")], mcp: false, local: false),
|
||||
cmd!("wait", World, "wait TICKS", "wait until TICKS game ticks have passed (20 per second)",
|
||||
"Keys and mouse buttons held with key ... down or mouse ... down stay held while it waits. Release them with key ... up, release or stop.",
|
||||
@@ -438,10 +336,10 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
cmd!("mcp", Scripting, "mcp", "serve the Model Context Protocol on stdin/stdout, one tool per command",
|
||||
"Add it to an MCP client as a stdio server: command horizon-ctl, args [\"mcp\"] (with --socket PATH first if needed). The screenshot tool returns the image.",
|
||||
["horizon-ctl mcp"], &[], mcp: false, local: true),
|
||||
cmd!("bench", Scripting, "bench SECONDS", "reset frame stats, wait, reply with the summary", "", ["horizon-ctl bench 10"], &[p("seconds", Ty::Num, "how long to measure")], mcp: true, local: false),
|
||||
cmd!("stats", Scripting, "stats [reset]", "frame-time summary since the last reset", "", ["horizon-ctl stats"], &[opt("reset", Ty::Bool, "start counting again")], mcp: true, local: false),
|
||||
cmd!("bench", Scripting, "bench SECONDS", "reset frame stats, wait, reply with the summary", "", ["horizon-ctl bench 10"], &[], mcp: false, local: false),
|
||||
cmd!("stats", Scripting, "stats [reset]", "frame-time summary since the last reset", "", ["horizon-ctl stats"], &[], mcp: false, local: false),
|
||||
cmd!("fly", Scripting, "fly X1 Y1 Z1 X2 Y2 Z2 SECONDS [PITCH [raw]]", "fly the camera along a line, then reply with the frame summary",
|
||||
"The reply's tick_ms and tick_ms_max are the world's smoothed tick time over the flight (the server thread's when single player runs on one). With raw, the reply also lists every frame time in milliseconds (frame_ms).",
|
||||
"With raw, the reply also lists every frame time in milliseconds (frame_ms).",
|
||||
["horizon-ctl fly 0 100 0 500 100 0 20"], &[], mcp: false, local: false),
|
||||
];
|
||||
|
||||
@@ -464,9 +362,8 @@ pub fn overview() -> String {
|
||||
}
|
||||
out.push_str(
|
||||
"\nEvery reply is one JSON line. Errors are {\"error\": \"...\"}; the exit status is 0 on\n\
|
||||
success, 1 when the game replied with an error, 2 for a usage error, 3 when the\n\
|
||||
game cannot be reached or started and 4 while No AI is on (the launcher's settings).\n\
|
||||
Actions (goto, break, place...) block until done.\n\
|
||||
success, 1 when the game replied with an error, 2 for a usage error and 3 when the\n\
|
||||
game cannot be reached or started. Actions (goto, break, place...) block until done.\n\
|
||||
horizon-ctl help --game lists the running game's own commands, test hooks included.\n\
|
||||
The socket is $HORIZON_CONTROL, else $XDG_RUNTIME_DIR/horizon-control.sock (on Windows\n\
|
||||
%TEMP%\\horizon-control.json, a localhost TCP port with a token; any path ending in .json\n\
|
||||
@@ -501,73 +398,6 @@ pub fn detail(c: &Cmd) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// The generated reference, docs/horizon-ctl-commands.md (`horizon-ctl help --markdown`).
|
||||
pub fn markdown() -> String {
|
||||
let cell = |s: &str| s.replace('|', "\\|");
|
||||
let mut out = String::from(
|
||||
"<!-- Generated by `horizon-ctl help --markdown` from the command table in \
|
||||
crates/horizon-client/src/bin/horizon-ctl/commands.rs: edit the table, then regenerate. -->\n\
|
||||
# horizon-ctl commands\n\n\
|
||||
Every command `horizon-ctl` takes, its MCP tool in `horizon-ctl mcp` (the launcher's \
|
||||
`horizon-launcher mcp` offers the same tools as `game_<tool>`, all but `launch`) and its \
|
||||
arguments. Positional arguments come first, in order; `--flags` after them. \
|
||||
`horizon-ctl help --game` lists the game's own commands, test hooks included. \
|
||||
Design rules and the full inventory: [agent-control.md](agent-control.md).\n",
|
||||
);
|
||||
for group in Group::ALL {
|
||||
out.push_str(&format!(
|
||||
"\n## {}\n\n| Command | MCP tool | What it does | Arguments |\n| --- | --- | --- | --- |\n",
|
||||
group.title()
|
||||
));
|
||||
for c in COMMANDS.iter().filter(|c| c.group == group) {
|
||||
let tool = if c.mcp {
|
||||
let mut t = format!("`{}`", tool_name(c));
|
||||
if DESTRUCTIVE.contains(&c.name) {
|
||||
t.push_str(" (destructive)");
|
||||
} else if OPEN_WORLD.contains(&c.name) {
|
||||
t.push_str(" (network)");
|
||||
}
|
||||
t
|
||||
} else {
|
||||
"CLI only".to_owned()
|
||||
};
|
||||
let args: Vec<String> = c
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let ty = match p.ty {
|
||||
Ty::Int => "integer".to_owned(),
|
||||
Ty::Num => "number".to_owned(),
|
||||
Ty::Str => "string".to_owned(),
|
||||
Ty::Rest => "text".to_owned(),
|
||||
Ty::Bool => "boolean".to_owned(),
|
||||
Ty::Enum(v) => v.join(", "),
|
||||
};
|
||||
let name = if p.flag {
|
||||
format!("--{}", p.name)
|
||||
} else {
|
||||
p.name.to_owned()
|
||||
};
|
||||
let need = if p.required { ", required" } else { "" };
|
||||
format!("`{name}` ({ty}{need}): {}", cell(p.help))
|
||||
})
|
||||
.collect();
|
||||
out.push_str(&format!(
|
||||
"| `{}` | {} | {} | {} |\n",
|
||||
cell(c.usage),
|
||||
tool,
|
||||
cell(&capitalize(c.summary)),
|
||||
if args.is_empty() {
|
||||
"none".to_owned()
|
||||
} else {
|
||||
args.join("<br>")
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn capitalize(s: &str) -> String {
|
||||
let mut chars = s.chars();
|
||||
chars
|
||||
@@ -806,43 +636,6 @@ mod tests {
|
||||
assert_eq!(tool_name(find("look-at-entity").unwrap()), "look_at_entity");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_generated_reference_is_up_to_date() {
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../docs/horizon-ctl-commands.md"
|
||||
);
|
||||
let on_disk = std::fs::read_to_string(path).unwrap_or_default();
|
||||
assert!(
|
||||
on_disk == markdown(),
|
||||
"docs/horizon-ctl-commands.md is out of date: run `horizon-ctl help --markdown > docs/horizon-ctl-commands.md`"
|
||||
);
|
||||
for c in COMMANDS {
|
||||
assert!(
|
||||
on_disk.contains(&format!("`{}", c.usage.replace('|', "\\|"))),
|
||||
"{} undocumented",
|
||||
c.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destructive_and_network_tools_exist() {
|
||||
for name in DESTRUCTIVE.iter().chain(OPEN_WORLD) {
|
||||
assert!(find(name).is_some_and(|c| c.mcp), "{name}");
|
||||
}
|
||||
let delete = find("world-delete").unwrap();
|
||||
assert_eq!(
|
||||
line_for(delete, &json!({ "folder": "A", "confirm": true })).unwrap(),
|
||||
["world-delete", "A", "--confirm"]
|
||||
);
|
||||
let lan = find("lan").unwrap();
|
||||
assert_eq!(
|
||||
line_for(lan, &json!({ "action": "open", "commands": true })).unwrap(),
|
||||
["lan", "open", "commands"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_mentions_every_command() {
|
||||
let text = overview();
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn prepared_resources(dir: &Path) -> Option<PathBuf> {
|
||||
.ok()
|
||||
.and_then(|t| serde_json::from_str::<Value>(&t).ok())
|
||||
.and_then(|v| v["minecraft"]["version"].as_str().map(str::to_owned))
|
||||
.unwrap_or_else(|| horizon_core::VANILLA_VERSION.into());
|
||||
.unwrap_or_else(|| "26.3".into());
|
||||
let resources = dir.join(".local/horizon-client").join(version);
|
||||
resources
|
||||
.join("horizon-resources.json")
|
||||
|
||||
@@ -20,8 +20,6 @@ use serde_json::{Value, json};
|
||||
const GAME_ERROR: u8 = 1;
|
||||
const USAGE: u8 = 2;
|
||||
const UNREACHABLE: u8 = 3;
|
||||
/// No AI is on (`horizon_core::no_ai`): agent control is off.
|
||||
const DENIED: u8 = 4;
|
||||
|
||||
/// A failure, with the exit status it maps to.
|
||||
#[derive(Debug)]
|
||||
@@ -36,13 +34,6 @@ impl Failure {
|
||||
status: USAGE,
|
||||
}
|
||||
}
|
||||
/// No AI is on in the game's home.
|
||||
fn denied() -> Self {
|
||||
Self {
|
||||
reply: json!({ "error": horizon_core::no_ai::REFUSAL, "kind": "denied" }),
|
||||
status: DENIED,
|
||||
}
|
||||
}
|
||||
fn unreachable(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
reply: json!({ "error": message.into(), "kind": "connection" }),
|
||||
@@ -107,8 +98,6 @@ impl Client {
|
||||
pub struct Session {
|
||||
pub socket: PathBuf,
|
||||
pub timeout: Option<Duration>,
|
||||
/// The game's home, where No AI is read (`horizon_core::no_ai`).
|
||||
pub home: PathBuf,
|
||||
client: Option<Client>,
|
||||
}
|
||||
|
||||
@@ -117,14 +106,9 @@ impl Session {
|
||||
Self {
|
||||
socket,
|
||||
timeout,
|
||||
home: horizon_client::logging::game_home(),
|
||||
client: None,
|
||||
}
|
||||
}
|
||||
/// A failure while No AI is on: every command refuses (help aside).
|
||||
pub fn allowed(&self) -> Result<(), Failure> {
|
||||
horizon_core::no_ai::check(&self.home).map_err(|_| Failure::denied())
|
||||
}
|
||||
fn client(&mut self) -> Result<&mut Client, Failure> {
|
||||
if self.client.is_none() {
|
||||
self.client = Some(Client::connect(&self.socket, self.timeout)?);
|
||||
@@ -153,7 +137,6 @@ impl Session {
|
||||
let Some(name) = words.first().map(String::as_str) else {
|
||||
return Err(Failure::usage("no command; try horizon-ctl help"));
|
||||
};
|
||||
self.allowed()?;
|
||||
if words.iter().skip(1).any(|w| w == "--help" || w == "-h") {
|
||||
return Err(Failure::usage(
|
||||
"use horizon-ctl COMMAND --help outside scripts",
|
||||
@@ -191,18 +174,6 @@ impl Session {
|
||||
ready["launch"] = reply;
|
||||
Ok(ready)
|
||||
}
|
||||
"interact-entity" => {
|
||||
let id = match words {
|
||||
[_, id] if id.parse::<i64>().is_ok() => id,
|
||||
_ => return Err(Failure::usage(format!("usage: {}", usage(name)))),
|
||||
};
|
||||
self.game(&format!("look-at entity {id}"))?;
|
||||
self.game("mouse right click")?;
|
||||
self.game("wait 2")?;
|
||||
let mut screen = self.game("screen")?;
|
||||
screen["ok"] = json!(true);
|
||||
Ok(screen)
|
||||
}
|
||||
"hold" => {
|
||||
let (key, seconds) = match words {
|
||||
[_, key, _] => (key.clone(), number(2, "SECONDS")?),
|
||||
@@ -212,7 +183,7 @@ impl Session {
|
||||
thread::sleep(Duration::from_secs_f64(seconds.max(0.0)));
|
||||
self.game(&format!("key {key} up"))
|
||||
}
|
||||
"logs" => logs(&self.home, &words[1..]),
|
||||
"logs" => logs(&horizon_client::logging::game_home(), &words[1..]),
|
||||
"wait-ready" => {
|
||||
let limit = if words.len() > 1 {
|
||||
number(1, "SECONDS")?
|
||||
@@ -244,12 +215,7 @@ impl Session {
|
||||
let deadline = Instant::now() + Duration::from_secs_f64(limit.max(0.0));
|
||||
loop {
|
||||
let status = self.game("status")?;
|
||||
// On a server (single player's server thread too), also the
|
||||
// level around the player.
|
||||
if !status["player"].is_null()
|
||||
&& status["pending_chunks"] == 0
|
||||
&& status["server"]["loaded"] != false
|
||||
{
|
||||
if !status["player"].is_null() && status["pending_chunks"] == 0 {
|
||||
return Ok(status);
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
@@ -446,10 +412,6 @@ fn main() -> ExitCode {
|
||||
print!("{}", commands::overview());
|
||||
0
|
||||
}
|
||||
Some("help") if args.get(1).is_some_and(|a| a == "--markdown") => {
|
||||
print!("{}", commands::markdown());
|
||||
0
|
||||
}
|
||||
Some("help") if args.get(1).is_none_or(|a| a != "--game") => match args.get(1) {
|
||||
None => {
|
||||
print!("{}", commands::overview());
|
||||
@@ -484,10 +446,6 @@ fn main() -> ExitCode {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("mcp") if session.allowed().is_err() => {
|
||||
eprintln!("horizon-ctl mcp: {}", horizon_core::no_ai::REFUSAL);
|
||||
DENIED
|
||||
}
|
||||
Some("mcp") => match mcp::serve(&mut session) {
|
||||
Ok(()) => 0,
|
||||
Err(e) => {
|
||||
@@ -618,27 +576,6 @@ mod tests {
|
||||
assert_eq!(f.status, USAGE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_ai_refuses_every_command() {
|
||||
let home = std::env::temp_dir().join(format!("horizon-ctl-no-ai-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&home);
|
||||
let mut s = Session::new(std::env::temp_dir().join("horizon-ctl-nobody.sock"), None);
|
||||
s.home = home.clone();
|
||||
assert_eq!(
|
||||
s.run(&["state".to_owned()]).unwrap_err().status,
|
||||
UNREACHABLE
|
||||
);
|
||||
horizon_core::no_ai::set(&home, true).unwrap();
|
||||
for words in [vec!["state"], vec!["logs"], vec!["launch", "--world", "A"]] {
|
||||
let words: Vec<String> = words.into_iter().map(str::to_owned).collect();
|
||||
let f = s.run(&words).unwrap_err();
|
||||
assert_eq!(f.status, DENIED, "{words:?}");
|
||||
assert_eq!(f.reply["kind"], "denied");
|
||||
assert_eq!(f.reply["error"], horizon_core::no_ai::REFUSAL);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screenshot_paths_become_absolute() {
|
||||
let mut words = vec!["screenshot".to_owned(), "a.png".to_owned()];
|
||||
|
||||
@@ -23,13 +23,7 @@ fn tools() -> Vec<Value> {
|
||||
schema = json!({ "type": "object", "properties": {} });
|
||||
description = "Capture the game window and return it as a PNG image.".into();
|
||||
}
|
||||
let mut tool = json!({ "name": commands::tool_name(c), "description": description, "inputSchema": schema });
|
||||
if commands::DESTRUCTIVE.contains(&c.name) {
|
||||
tool["annotations"] = json!({ "destructiveHint": true });
|
||||
} else if commands::OPEN_WORLD.contains(&c.name) {
|
||||
tool["annotations"] = json!({ "openWorldHint": true });
|
||||
}
|
||||
tool
|
||||
json!({ "name": commands::tool_name(c), "description": description, "inputSchema": schema })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -136,7 +130,7 @@ impl Session {
|
||||
"initialize" => Ok(json!({
|
||||
"protocolVersion": params.get("protocolVersion").and_then(Value::as_str).unwrap_or(PROTOCOL),
|
||||
"capabilities": { "tools": {} },
|
||||
"serverInfo": { "name": "horizon-ctl", "version": horizon_core::release::VERSION },
|
||||
"serverInfo": { "name": "horizon-ctl", "version": env!("CARGO_PKG_VERSION") },
|
||||
"instructions": "Play Horizon, a Rust reimplementation of Minecraft Java Edition. Start with launch (it finds the game installed by the Horizon launcher and opens or creates a world), then read the world with state, inventory, screen, entities and find_block, and act with goto, look_at_block, break, place, interact, click_slot and click_button. Actions block until they finish and report what happened. Coordinates are block coordinates; yaw 0 faces south (+Z).",
|
||||
})),
|
||||
"ping" => Ok(json!({})),
|
||||
|
||||
@@ -182,102 +182,3 @@ impl Biomes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A 24-bit colour as sRGB components in 0..1.
|
||||
pub fn rgb(c: u32) -> [f32; 3] {
|
||||
[16, 8, 0].map(|shift| (c >> shift & 255) as f32 / 255.0)
|
||||
}
|
||||
|
||||
/// `GaussianSampler.GAUSSIAN_SAMPLE_KERNEL`.
|
||||
const GAUSSIAN_KERNEL: [f64; 7] = [0.0, 1.0, 4.0, 6.0, 4.0, 1.0, 0.0];
|
||||
|
||||
/// `EnvironmentAttributeProbe.tick`: `GaussianSampler.sample` over the noise
|
||||
/// biomes (quart cells) around `position`, in blocks. Each biome comes once
|
||||
/// with its summed weight, in the order it was first sampled, as
|
||||
/// `SpatialAttributeInterpolator` keeps them (zero weights included).
|
||||
pub fn gaussian_weights(
|
||||
position: [f64; 3],
|
||||
mut biome_at_quart: impl FnMut(i32, i32, i32) -> u16,
|
||||
) -> Vec<(u16, f64)> {
|
||||
let p = position.map(|c| c * 0.25 - 0.5);
|
||||
let integral = p.map(|c| c.floor() as i32);
|
||||
let relative: [f64; 3] = std::array::from_fn(|i| p[i] - f64::from(integral[i]));
|
||||
let weight = |axis: usize, i: usize| {
|
||||
let (start, end) = (GAUSSIAN_KERNEL[i + 1], GAUSSIAN_KERNEL[i]);
|
||||
start + relative[axis] * (end - start)
|
||||
};
|
||||
let mut weights: Vec<(u16, f64)> = Vec::new();
|
||||
for z in 0..6 {
|
||||
let wz = weight(2, z);
|
||||
for x in 0..6 {
|
||||
let wx = weight(0, x);
|
||||
for y in 0..6 {
|
||||
let wy = weight(1, y);
|
||||
let biome = biome_at_quart(
|
||||
integral[0] - 2 + x as i32,
|
||||
integral[1] - 2 + y as i32,
|
||||
integral[2] - 2 + z as i32,
|
||||
);
|
||||
let w = wx * wy * wz;
|
||||
match weights.iter_mut().find(|(b, _)| *b == biome) {
|
||||
Some((_, total)) => *total += w,
|
||||
None => weights.push((biome, w)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
weights
|
||||
}
|
||||
|
||||
/// `SpatialAttributeInterpolator.applyAttributeLayer` for an RGB colour
|
||||
/// (`ARGB.srgbLerp`: a component-wise lerp of the sRGB values).
|
||||
pub fn blend_rgb(weights: &[(u16, f64)], mut value: impl FnMut(u16) -> [f32; 3]) -> [f32; 3] {
|
||||
let mut result: Option<[f32; 3]> = None;
|
||||
let mut accumulated = 0.0;
|
||||
for &(biome, weight) in weights {
|
||||
let v = value(biome);
|
||||
accumulated += weight;
|
||||
result = Some(match result {
|
||||
None => v,
|
||||
Some(r) => {
|
||||
let t = (weight / accumulated) as f32;
|
||||
std::array::from_fn(|i| r[i] + (v[i] - r[i]) * t)
|
||||
}
|
||||
});
|
||||
}
|
||||
result.unwrap_or([0.0; 3])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod blend_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_biome_keeps_its_colour() {
|
||||
let weights = gaussian_weights([10.3, 64.0, -7.9], |_, _, _| 4);
|
||||
assert_eq!(weights.len(), 1);
|
||||
// The kernel sums to 16 per axis whatever the offset.
|
||||
assert!((weights[0].1 - 16f64.powi(3)).abs() < 1e-9);
|
||||
assert_eq!(blend_rgb(&weights, |_| [0.2, 0.4, 0.6]), [0.2, 0.4, 0.6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_changes_gradually_across_a_boundary() {
|
||||
// Biome 1 for quart x >= 0 (block x >= 0), biome 0 below.
|
||||
let at = |x: f64| {
|
||||
let w = gaussian_weights([x, 40.0, 0.0], |qx, _, _| u16::from(qx >= 0));
|
||||
blend_rgb(&w, |b| if b == 1 { [1.0, 0.0, 0.0] } else { [0.0; 3] })[0]
|
||||
};
|
||||
assert_eq!(at(-12.0), 0.0);
|
||||
assert_eq!(at(12.0), 1.0);
|
||||
// Halfway at the boundary, rising steadily through it.
|
||||
assert!((at(0.0) - 0.5).abs() < 1e-5, "{}", at(0.0));
|
||||
let mut last = 0.0;
|
||||
for i in -16..=16 {
|
||||
let v = at(f64::from(i) * 0.5);
|
||||
assert!(v >= last - 1e-6, "{v} < {last} at {i}");
|
||||
last = v;
|
||||
}
|
||||
assert!(at(-2.0) > 0.0 && at(-2.0) < 0.5);
|
||||
}
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
//! The world's block entities and the order they tick in: vanilla's
|
||||
//! `Level.blockEntityTickers`, kept beside the block entity map so a tick
|
||||
//! visits only the block entities that tick, in vanilla's order, instead of
|
||||
//! scanning the whole map once per kind.
|
||||
//!
|
||||
//! The list follows `LevelChunk.updateBlockEntityTicker` and
|
||||
//! `Level.tickBlockEntities`:
|
||||
//! - a block entity whose block state has a ticker gets a slot at the end of
|
||||
//! the list (through `pendingBlockEntityTickers`, so a slot made during a
|
||||
//! tick first ticks on the next one);
|
||||
//! - replacing the block entity at a position (`setBlockEntity`), or changing
|
||||
//! its block state to another state with a ticker, keeps its slot
|
||||
//! (`RebindableTickingBlockEntityWrapper.rebind`);
|
||||
//! - removing it (`removeBlockEntity`), unloading its chunk
|
||||
//! (`clearAllBlockEntities`) or changing its state to one without a ticker
|
||||
//! (a jukebox losing its record) frees the slot; a later ticker there is
|
||||
//! appended again;
|
||||
//! - a loaded chunk's block entities are added in the iteration order of its
|
||||
//! `Object2ObjectOpenHashMap<BlockPos, BlockEntity>`
|
||||
//! (`registerAllBlockEntitiesAfterLevelLoad`), filled in the order the chunk
|
||||
//! listed them.
|
||||
//!
|
||||
//! Which states tick is a table by state id, built by the game from the block
|
||||
//! registry; whether a slot is made is decided when the slot is promoted at
|
||||
//! the start of the next tick, from the block state then.
|
||||
use crate::containers::BlockEntity;
|
||||
use crate::fast_hash::{FxHashMap, FxHashSet};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type Pos = (i32, i32, i32);
|
||||
|
||||
/// What a block state's block entity does each tick (`BlockState.getTicker`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Ticking {
|
||||
/// No ticker, and the block has no block entity that could have one.
|
||||
#[default]
|
||||
No,
|
||||
/// A ticker.
|
||||
Yes,
|
||||
/// The block's block entity, but this state has no ticker (a jukebox
|
||||
/// without a record): a state change to it frees the slot.
|
||||
Idle,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BlockEntities {
|
||||
map: HashMap<Pos, BlockEntity>,
|
||||
/// `Level.blockEntityTickers`: positions and the slot each was given.
|
||||
tickers: Vec<(Pos, u32)>,
|
||||
/// `LevelChunk.tickersInLevel`: the live slot of each ticking position.
|
||||
live: FxHashMap<Pos, u32>,
|
||||
/// Positions that may need a slot, in the order vanilla would have made
|
||||
/// it (`pendingBlockEntityTickers`), each with a token; decided at the
|
||||
/// next promotion from the block state then.
|
||||
added: Vec<(Pos, u32)>,
|
||||
/// The token of each position's pending entry: an entry whose token is
|
||||
/// not here any more was cancelled.
|
||||
pending: FxHashMap<Pos, u32>,
|
||||
next_slot: u32,
|
||||
/// Whether a slot was freed since the list was last swept: otherwise
|
||||
/// every slot in it is still live, unless its block entity is `out`.
|
||||
freed: bool,
|
||||
/// Positions whose block entity `take` took and `put` has not brought
|
||||
/// back.
|
||||
out: FxHashSet<Pos>,
|
||||
/// `Ticking` by block state id; empty until the game builds it.
|
||||
states: Vec<Ticking>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for BlockEntities {
|
||||
type Target = HashMap<Pos, BlockEntity>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a BlockEntities {
|
||||
type Item = (&'a Pos, &'a BlockEntity);
|
||||
type IntoIter = std::collections::hash_map::Iter<'a, Pos, BlockEntity>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.map.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// `Vec3i.hashCode`.
|
||||
fn java_hash((x, y, z): Pos) -> i32 {
|
||||
y.wrapping_add(z.wrapping_mul(31))
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(x)
|
||||
}
|
||||
/// `HashCommon.mix`.
|
||||
fn mix(x: i32) -> usize {
|
||||
let h = (x as u32).wrapping_mul(0x9E37_79B9);
|
||||
(h ^ (h >> 16)) as usize
|
||||
}
|
||||
|
||||
/// The order `values().forEach` visits a `new Object2ObjectOpenHashMap<>()`
|
||||
/// keyed by these positions, put in this order (repeats keep their first
|
||||
/// slot): open addressing with linear probing, a rehash to twice the size
|
||||
/// past three quarters full, and iteration from the last slot down.
|
||||
pub fn chunk_map_order(positions: impl IntoIterator<Item = Pos>) -> Vec<Pos> {
|
||||
// HashCommon.arraySize(16, .75f), and maxFill of each size.
|
||||
let mut slots: Vec<Option<Pos>> = vec![None; 32];
|
||||
let max_fill = |n: usize| (n * 3).div_ceil(4).min(n - 1);
|
||||
let mut size = 0;
|
||||
for p in positions {
|
||||
let mask = slots.len() - 1;
|
||||
let mut at = mix(java_hash(p)) & mask;
|
||||
let mut known = false;
|
||||
while let Some(k) = slots[at] {
|
||||
if k == p {
|
||||
known = true;
|
||||
break;
|
||||
}
|
||||
at = (at + 1) & mask;
|
||||
}
|
||||
if known {
|
||||
continue;
|
||||
}
|
||||
slots[at] = Some(p);
|
||||
// `if (size++ >= maxFill) rehash(arraySize(size + 1, f))`, which
|
||||
// puts the keys back from the last slot down.
|
||||
size += 1;
|
||||
if size > max_fill(slots.len()) {
|
||||
let n = ((size + 1) * 4).div_ceil(3).next_power_of_two();
|
||||
let old = std::mem::replace(&mut slots, vec![None; n]);
|
||||
for k in old.into_iter().rev().flatten() {
|
||||
let mut at = mix(java_hash(k)) & (n - 1);
|
||||
while slots[at].is_some() {
|
||||
at = (at + 1) & (n - 1);
|
||||
}
|
||||
slots[at] = Some(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
slots.into_iter().rev().flatten().collect()
|
||||
}
|
||||
|
||||
impl BlockEntities {
|
||||
/// Whether the ticking table has been built.
|
||||
pub fn has_states(&self) -> bool {
|
||||
!self.states.is_empty()
|
||||
}
|
||||
pub fn set_states(&mut self, states: Vec<Ticking>) {
|
||||
self.states = states;
|
||||
}
|
||||
pub fn take_states(&mut self) -> Vec<Ticking> {
|
||||
std::mem::take(&mut self.states)
|
||||
}
|
||||
pub fn ticking(&self, state: u32) -> Ticking {
|
||||
self.states.get(state as usize).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn next(&mut self) -> u32 {
|
||||
let n = self.next_slot;
|
||||
self.next_slot = n.wrapping_add(1);
|
||||
n
|
||||
}
|
||||
/// A ticker may be made for `pos` now (`updateBlockEntityTicker`).
|
||||
fn add(&mut self, pos: Pos) {
|
||||
let token = self.next();
|
||||
self.pending.insert(pos, token);
|
||||
self.added.push((pos, token));
|
||||
}
|
||||
/// `removeBlockEntityTicker`: no slot, made or pending.
|
||||
fn cancel(&mut self, pos: &Pos) {
|
||||
self.freed |= self.live.remove(pos).is_some();
|
||||
self.pending.remove(pos);
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, pos: &Pos) -> Option<&mut BlockEntity> {
|
||||
self.map.get_mut(pos)
|
||||
}
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pos, &mut BlockEntity)> {
|
||||
self.map.iter_mut()
|
||||
}
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut BlockEntity> {
|
||||
self.map.values_mut()
|
||||
}
|
||||
/// `LevelChunk.setBlockEntity` (+ `addAndRegisterBlockEntity`): a new
|
||||
/// block entity may get a slot; one replacing another keeps its slot.
|
||||
pub fn insert(&mut self, pos: Pos, entity: BlockEntity) -> Option<BlockEntity> {
|
||||
let old = self.map.insert(pos, entity);
|
||||
if old.is_none() {
|
||||
self.add(pos);
|
||||
}
|
||||
old
|
||||
}
|
||||
/// The block entity at `pos`, made by `make` when there is none.
|
||||
pub fn get_or_insert_with(
|
||||
&mut self,
|
||||
pos: Pos,
|
||||
make: impl FnOnce() -> BlockEntity,
|
||||
) -> &mut BlockEntity {
|
||||
if !self.map.contains_key(&pos) {
|
||||
self.add(pos);
|
||||
}
|
||||
self.map.entry(pos).or_insert_with(make)
|
||||
}
|
||||
/// `LevelChunk.removeBlockEntity`: the block entity and its slot.
|
||||
pub fn remove(&mut self, pos: &Pos) -> Option<BlockEntity> {
|
||||
self.cancel(pos);
|
||||
self.map.remove(pos)
|
||||
}
|
||||
/// The block entity at `pos`, out of the map for a while: its slot stays
|
||||
/// for `put` to bring it back (a tick working on it).
|
||||
pub fn take(&mut self, pos: &Pos) -> Option<BlockEntity> {
|
||||
let taken = self.map.remove(pos);
|
||||
if taken.is_some() {
|
||||
self.out.insert(*pos);
|
||||
}
|
||||
taken
|
||||
}
|
||||
/// Brings back what `take` took, in its slot.
|
||||
pub fn put(&mut self, pos: Pos, entity: BlockEntity) {
|
||||
self.out.remove(&pos);
|
||||
if self.map.insert(pos, entity).is_none()
|
||||
&& !self.live.contains_key(&pos)
|
||||
&& !self.pending.contains_key(&pos)
|
||||
{
|
||||
self.add(pos);
|
||||
}
|
||||
}
|
||||
/// Removes the block entities `keep` refuses, and their slots
|
||||
/// (`clearAllBlockEntities` of unloaded chunks).
|
||||
pub fn retain(&mut self, mut keep: impl FnMut(&Pos, &mut BlockEntity) -> bool) {
|
||||
let (live, pending, freed) = (&mut self.live, &mut self.pending, &mut self.freed);
|
||||
self.map.retain(|p, e| {
|
||||
let kept = keep(p, e);
|
||||
if !kept {
|
||||
*freed |= live.remove(p).is_some();
|
||||
pending.remove(p);
|
||||
}
|
||||
kept
|
||||
});
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
self.tickers.clear();
|
||||
self.live.clear();
|
||||
self.added.clear();
|
||||
self.pending.clear();
|
||||
self.out.clear();
|
||||
self.freed = false;
|
||||
}
|
||||
/// A loaded or generated chunk's block entities, in the order the chunk
|
||||
/// lists them; they tick in its block entity map's order
|
||||
/// (`registerAllBlockEntitiesAfterLevelLoad`).
|
||||
pub fn load_chunk(&mut self, entities: Vec<(Pos, BlockEntity)>) {
|
||||
let order = chunk_map_order(entities.iter().map(|(p, _)| *p));
|
||||
for (p, e) in entities {
|
||||
self.map.insert(p, e);
|
||||
}
|
||||
for p in order {
|
||||
self.add(p);
|
||||
}
|
||||
}
|
||||
/// `LevelChunk.setBlockState` on a block entity's position, from state
|
||||
/// `old` to `new`: a ticker made, kept or dropped.
|
||||
pub fn state_changed(&mut self, pos: Pos, old: u32, new: u32) {
|
||||
if self.states.is_empty() || !self.map.contains_key(&pos) {
|
||||
return;
|
||||
}
|
||||
match self.ticking(new) {
|
||||
// A new ticker, unless one was made or is pending (rebound).
|
||||
Ticking::Yes => {
|
||||
if self.ticking(old) != Ticking::Yes && !self.live.contains_key(&pos) {
|
||||
self.add(pos);
|
||||
}
|
||||
}
|
||||
Ticking::Idle => self.cancel(&pos),
|
||||
// Another block: the game removes or replaces the block entity.
|
||||
Ticking::No => {}
|
||||
}
|
||||
}
|
||||
/// The start of `Level.tickBlockEntities`: pending slots join the list
|
||||
/// and freed ones leave it. `state` reads the block state at a position.
|
||||
/// Returns the slots to tick, in order; check each with `is_live` before
|
||||
/// ticking it, as the ticks before it can free it.
|
||||
pub fn tickers(&mut self, state: impl Fn(Pos) -> u32) -> Vec<(Pos, u32)> {
|
||||
for (pos, token) in std::mem::take(&mut self.added) {
|
||||
if self.pending.get(&pos) != Some(&token) {
|
||||
continue;
|
||||
}
|
||||
self.pending.remove(&pos);
|
||||
if self.live.contains_key(&pos)
|
||||
|| !self.map.contains_key(&pos)
|
||||
|| self.ticking(state(pos)) != Ticking::Yes
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let slot = self.next();
|
||||
self.live.insert(pos, slot);
|
||||
self.tickers.push((pos, slot));
|
||||
}
|
||||
if !std::mem::take(&mut self.freed) && self.out.is_empty() {
|
||||
return self.tickers.clone();
|
||||
}
|
||||
self.out.clear();
|
||||
let (live, map) = (&mut self.live, &self.map);
|
||||
self.tickers.retain(|(p, slot)| {
|
||||
let mine = live.get(p) == Some(slot);
|
||||
if mine && !map.contains_key(p) {
|
||||
live.remove(p);
|
||||
}
|
||||
mine && map.contains_key(p)
|
||||
});
|
||||
self.tickers.clone()
|
||||
}
|
||||
/// Whether a slot from `tickers` still ticks.
|
||||
pub fn is_live(&self, (pos, slot): (Pos, u32)) -> bool {
|
||||
self.live.get(&pos) == Some(&slot) && self.map.contains_key(&pos)
|
||||
}
|
||||
/// Frees the slot at `pos` (the block entity stays).
|
||||
pub fn forget(&mut self, pos: &Pos) {
|
||||
self.cancel(pos);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn hopper() -> BlockEntity {
|
||||
BlockEntity::Chest(vec![None; 5])
|
||||
}
|
||||
/// States: 0 air, 1 ticks, 2 idle.
|
||||
fn level() -> BlockEntities {
|
||||
let mut b = BlockEntities::default();
|
||||
b.set_states(vec![Ticking::No, Ticking::Yes, Ticking::Idle]);
|
||||
b
|
||||
}
|
||||
fn order(b: &mut BlockEntities, states: &HashMap<Pos, u32>) -> Vec<Pos> {
|
||||
b.tickers(|p| states.get(&p).copied().unwrap_or(0))
|
||||
.into_iter()
|
||||
.map(|(p, _)| p)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots_follow_vanilla_lifecycle() {
|
||||
let mut b = level();
|
||||
let mut s = HashMap::new();
|
||||
for p in [(0, 0, 0), (1, 0, 0), (2, 0, 0)] {
|
||||
s.insert(p, 1);
|
||||
b.insert(p, hopper());
|
||||
}
|
||||
assert_eq!(order(&mut b, &s), [(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
|
||||
// Replacing keeps the slot; removing and placing again appends.
|
||||
b.insert((0, 0, 0), hopper());
|
||||
b.remove(&(1, 0, 0));
|
||||
b.insert((1, 0, 0), hopper());
|
||||
assert_eq!(order(&mut b, &s), [(0, 0, 0), (2, 0, 0), (1, 0, 0)]);
|
||||
// Take and put keep it; an idle state frees it, ticking again appends.
|
||||
let e = b.take(&(0, 0, 0)).unwrap();
|
||||
b.put((0, 0, 0), e);
|
||||
assert_eq!(order(&mut b, &s), [(0, 0, 0), (2, 0, 0), (1, 0, 0)]);
|
||||
s.insert((2, 0, 0), 2);
|
||||
b.state_changed((2, 0, 0), 1, 2);
|
||||
assert_eq!(order(&mut b, &s), [(0, 0, 0), (1, 0, 0)]);
|
||||
s.insert((2, 0, 0), 1);
|
||||
b.state_changed((2, 0, 0), 2, 1);
|
||||
assert_eq!(order(&mut b, &s), [(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
|
||||
// A slot freed during a tick is not live any more.
|
||||
let slots = b.tickers(|p| s.get(&p).copied().unwrap_or(0));
|
||||
b.retain(|p, _| *p != (1, 0, 0));
|
||||
assert!(b.is_live(slots[0]) && !b.is_live(slots[1]));
|
||||
// A block entity taken and never brought back loses its slot.
|
||||
b.take(&(0, 0, 0));
|
||||
assert_eq!(order(&mut b, &s), [(2, 0, 0)]);
|
||||
assert_eq!(order(&mut b, &s), [(2, 0, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loads_in_open_hash_map_order() {
|
||||
// A few keys: their mixed hashes over 32 slots, read from the top.
|
||||
let positions = [
|
||||
(0, 64, 0),
|
||||
(1, 64, 0),
|
||||
(0, 64, 1),
|
||||
(15, -64, 15),
|
||||
(3, 70, 9),
|
||||
];
|
||||
let slot = |p: &Pos| mix(java_hash(*p)) & 31;
|
||||
let mut expected = positions.to_vec();
|
||||
expected.sort_by_key(|p| std::cmp::Reverse(slot(p)));
|
||||
let distinct: std::collections::HashSet<usize> = positions.iter().map(slot).collect();
|
||||
assert_eq!(distinct.len(), positions.len());
|
||||
assert_eq!(chunk_map_order(positions), expected);
|
||||
// Past 24 keys the table doubles; every key stays, once.
|
||||
let many: Vec<Pos> = (0..100).map(|i| (i % 16, i / 16, 3)).collect();
|
||||
let order = chunk_map_order(many.iter().copied().chain(many.iter().copied()));
|
||||
assert_eq!(order.len(), 100);
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@
|
||||
//! (blocks, entities, game time) as closures, so the parity fixture can drive
|
||||
//! them with scripted inputs (`tools/test_block_entity_anim_parity.py`).
|
||||
use crate::block_entity_render::BlockAnim;
|
||||
use crate::fast_hash::FxHashMap;
|
||||
use horizon_core::{
|
||||
geometry::trig::{self, Mth},
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// `Direction` names by `get3DDataValue`.
|
||||
pub const DIRECTIONS: [&str; 6] = ["down", "up", "north", "south", "west", "east"];
|
||||
@@ -832,44 +832,38 @@ pub fn ticker(name: &str, prop: impl Fn(&str) -> Option<String>) -> Option<Ticke
|
||||
/// The animated block entities of a level, by position.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BlockEntityAnims {
|
||||
pub chests: FxHashMap<[i32; 3], ChestLid>,
|
||||
pub books: FxHashMap<[i32; 3], EnchantingBook>,
|
||||
pub chests: HashMap<[i32; 3], ChestLid>,
|
||||
pub books: HashMap<[i32; 3], EnchantingBook>,
|
||||
/// `EnchantingTableBlockEntity.RANDOM`, shared by every table.
|
||||
pub book_random: LegacyRandom,
|
||||
pub shulkers: FxHashMap<[i32; 3], Shulker>,
|
||||
pub bells: FxHashMap<[i32; 3], Bell>,
|
||||
pub conduits: FxHashMap<[i32; 3], Conduit>,
|
||||
pub skulls: FxHashMap<[i32; 3], Skull>,
|
||||
pub pots: FxHashMap<[i32; 3], PotWobble>,
|
||||
pub gateways: FxHashMap<[i32; 3], EndGateway>,
|
||||
pub shulkers: HashMap<[i32; 3], Shulker>,
|
||||
pub bells: HashMap<[i32; 3], Bell>,
|
||||
pub conduits: HashMap<[i32; 3], Conduit>,
|
||||
pub skulls: HashMap<[i32; 3], Skull>,
|
||||
pub pots: HashMap<[i32; 3], PotWobble>,
|
||||
pub gateways: HashMap<[i32; 3], EndGateway>,
|
||||
/// The integrated server's copies of the gateways (`portalTick`).
|
||||
pub gateway_servers: FxHashMap<[i32; 3], EndGateway>,
|
||||
pub gateway_servers: HashMap<[i32; 3], EndGateway>,
|
||||
/// The client level's random (conduit particles).
|
||||
pub random: LegacyRandom,
|
||||
/// [`Ticker`] by block state id (see [`BlockEntityAnims::classify`]).
|
||||
pub kinds: Vec<Option<Ticker>>,
|
||||
/// The drawn block entities with a ticker, sorted by position, and the
|
||||
/// renderer's block entity revision they were read at.
|
||||
pub present: Vec<([i32; 3], Ticker)>,
|
||||
pub present_revision: Option<u64>,
|
||||
}
|
||||
impl Default for BlockEntityAnims {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chests: FxHashMap::default(),
|
||||
books: FxHashMap::default(),
|
||||
chests: HashMap::new(),
|
||||
books: HashMap::new(),
|
||||
book_random: LegacyRandom::new(0x626f6f6b),
|
||||
shulkers: FxHashMap::default(),
|
||||
bells: FxHashMap::default(),
|
||||
conduits: FxHashMap::default(),
|
||||
skulls: FxHashMap::default(),
|
||||
pots: FxHashMap::default(),
|
||||
gateways: FxHashMap::default(),
|
||||
gateway_servers: FxHashMap::default(),
|
||||
shulkers: HashMap::new(),
|
||||
bells: HashMap::new(),
|
||||
conduits: HashMap::new(),
|
||||
skulls: HashMap::new(),
|
||||
pots: HashMap::new(),
|
||||
gateways: HashMap::new(),
|
||||
gateway_servers: HashMap::new(),
|
||||
random: LegacyRandom::new(0x6c6576656c),
|
||||
kinds: Vec::new(),
|
||||
present: Vec::new(),
|
||||
present_revision: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,20 +877,15 @@ impl BlockEntityAnims {
|
||||
.and_then(|s| ticker(&s.name, |k| s.properties.get(k).cloned()))
|
||||
})
|
||||
.collect();
|
||||
self.present_revision = None;
|
||||
}
|
||||
/// The ticker of a block state.
|
||||
pub fn ticker(&self, state: u32) -> Option<Ticker> {
|
||||
self.kinds.get(state as usize).copied().flatten()
|
||||
}
|
||||
/// Drops the state of block entities that are gone or changed kind:
|
||||
/// `present` holds the loaded block entities' tickers, sorted by position.
|
||||
pub fn retain_present(&mut self, present: &[([i32; 3], Ticker)]) {
|
||||
let is = |p: &[i32; 3], f: fn(&Ticker) -> bool| {
|
||||
present
|
||||
.binary_search_by_key(p, |(q, _)| *q)
|
||||
.is_ok_and(|i| f(&present[i].1))
|
||||
};
|
||||
/// `present` maps the loaded block entities to their tickers.
|
||||
pub fn retain_present(&mut self, present: &HashMap<[i32; 3], Ticker>) {
|
||||
let is = |p: &[i32; 3], f: fn(&Ticker) -> bool| present.get(p).is_some_and(f);
|
||||
self.chests.retain(|p, _| is(p, |t| *t == Ticker::Lid));
|
||||
self.books.retain(|p, _| is(p, |t| *t == Ticker::Book));
|
||||
self.shulkers
|
||||
|
||||
@@ -737,8 +737,6 @@ pub struct Scene<'a> {
|
||||
pub struct BlockEntityRenderer {
|
||||
models: HashMap<&'static str, ModelPart>,
|
||||
kinds: Vec<Kind>,
|
||||
// Closed chest geometry: dimension lighting, chest part, horizontal facing.
|
||||
closed_chests: [[[Vec<Vertex>; 4]; 3]; 2],
|
||||
/// Item id to its `PROVIDES_POTTERY_PATTERN` side texture (`Sheets.DECORATED_POT_MAPPER`).
|
||||
pot_patterns: HashMap<String, String>,
|
||||
/// `Block.shouldRenderFace` for End gateway faces (its occlusion pair cache needs a lock).
|
||||
@@ -819,43 +817,6 @@ impl BlockEntityRenderer {
|
||||
Ok(renderer)
|
||||
}
|
||||
fn new(models: HashMap<&'static str, ModelPart>, registry: &BlockRegistry) -> Self {
|
||||
let closed_chests = [false, true].map(|nether| {
|
||||
["chest", "double_chest_left", "double_chest_right"].map(|name| {
|
||||
[0.0f32, 90.0, 180.0, 270.0].map(|y_rot| {
|
||||
let mut vertices = Vec::new();
|
||||
if let Some(model) = models.get(name) {
|
||||
draw_part(
|
||||
model,
|
||||
"root",
|
||||
around(
|
||||
Mat4::from_rotation_y((-y_rot).to_radians()),
|
||||
Vec3::new(0.5, 0.0, 0.5),
|
||||
),
|
||||
&|name, p| {
|
||||
if name == "lid" || name == "lock" {
|
||||
p[3] = crate::block_entity_anim::chest_lid_x_rot(0.0);
|
||||
}
|
||||
},
|
||||
Paint {
|
||||
sprite: Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
tint: 0xFFFFFF,
|
||||
light: 0,
|
||||
lights: diffuse_lights(nether),
|
||||
},
|
||||
&mut vertices,
|
||||
);
|
||||
}
|
||||
vertices
|
||||
})
|
||||
})
|
||||
});
|
||||
let kinds = (0..registry.len() as u32)
|
||||
.map(|id| {
|
||||
registry.state(BlockStateId(id)).map_or(Kind::None, |s| {
|
||||
@@ -870,7 +831,6 @@ impl BlockEntityRenderer {
|
||||
Self {
|
||||
models,
|
||||
kinds,
|
||||
closed_chests,
|
||||
pot_patterns: HashMap::new(),
|
||||
visibility: None,
|
||||
}
|
||||
@@ -916,7 +876,7 @@ impl BlockEntityRenderer {
|
||||
.as_vec3();
|
||||
let base = Mat4::from_translation(origin);
|
||||
let packed = scene.world.light(pos[0], pos[1], pos[2]);
|
||||
self.render_one(scene, *pos, kind, base, packed, lights, false, true, out);
|
||||
self.render_one(scene, *pos, kind, base, packed, lights, false, out);
|
||||
}
|
||||
}
|
||||
/// One block entity at `pos` drawn under `base` with the given packed light
|
||||
@@ -932,21 +892,14 @@ impl BlockEntityRenderer {
|
||||
) {
|
||||
let kind = self.kind(state);
|
||||
let lights = diffuse_lights(scene.nether);
|
||||
self.render_one(scene, pos, kind, base, packed, lights, false, false, out);
|
||||
self.render_one(scene, pos, kind, base, packed, lights, false, out);
|
||||
}
|
||||
/// Whether this block state has a block-entity renderer here.
|
||||
pub fn renders(&self, state: u32) -> bool {
|
||||
!matches!(self.kind(state), Kind::None)
|
||||
}
|
||||
/// ChestRenderer and BannerRenderer submit their animated models for crumbling.
|
||||
pub fn render_breaking(
|
||||
&self,
|
||||
scene: &Scene,
|
||||
pos: [i32; 3],
|
||||
state: u32,
|
||||
stage: [f32; 2],
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
pub fn render_breaking(&self, scene: &Scene, pos: [i32; 3], state: u32, out: &mut Vec<Vertex>) {
|
||||
if !matches!(self.kind(state), Kind::Banner { .. } | Kind::Chest { .. }) {
|
||||
return;
|
||||
}
|
||||
@@ -962,10 +915,9 @@ impl BlockEntityRenderer {
|
||||
0xff,
|
||||
diffuse_lights(scene.nether),
|
||||
true,
|
||||
false,
|
||||
out,
|
||||
);
|
||||
crate::breaking::entity_geometry(&mut out[start..], origin, stage);
|
||||
crate::breaking::entity_geometry(&mut out[start..], origin);
|
||||
}
|
||||
/// `ChestRenderer.extractRenderState`'s openness for the chest of `state`
|
||||
/// at `pos`: `ChestBlock.combine` (`DoubleBlockCombiner.combineWithNeigbour`)
|
||||
@@ -1039,17 +991,7 @@ impl BlockEntityRenderer {
|
||||
let kind = self.kind(state);
|
||||
if !matches!(kind, Kind::None) {
|
||||
let lights = diffuse_lights(scene.nether);
|
||||
self.render_one(
|
||||
scene,
|
||||
[i32::MAX; 3],
|
||||
kind,
|
||||
base,
|
||||
packed,
|
||||
lights,
|
||||
false,
|
||||
false,
|
||||
out,
|
||||
);
|
||||
self.render_one(scene, [i32::MAX; 3], kind, base, packed, lights, false, out);
|
||||
}
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -1062,7 +1004,6 @@ impl BlockEntityRenderer {
|
||||
packed: u8,
|
||||
lights: (Vec3, Vec3),
|
||||
crumbling: bool,
|
||||
world_chests: bool,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let sprite = |name: &str| scene.sprites.get(name).copied();
|
||||
@@ -1166,23 +1107,6 @@ impl BlockEntityRenderer {
|
||||
let l = scene.world.light(other[0], other[1], other[2]);
|
||||
packed = (packed >> 4).max(l >> 4) << 4 | (packed & 15).max(l & 15);
|
||||
}
|
||||
if world_chests && open == 0.0 {
|
||||
if let Some(paint) = paint(texture, 0xFFFFFF, packed) {
|
||||
let vertices = &self.closed_chests[usize::from(scene.nether)]
|
||||
[*part as usize][*y_rot as usize / 90];
|
||||
let origin = base.w_axis.truncate();
|
||||
out.extend(vertices.iter().map(|v| Vertex {
|
||||
pos: (Vec3::from_array(v.pos) + origin).to_array(),
|
||||
uv: [
|
||||
paint.sprite.u0 + (paint.sprite.u1 - paint.sprite.u0) * v.uv[0],
|
||||
paint.sprite.v0 + (paint.sprite.v1 - paint.sprite.v0) * v.uv[1],
|
||||
],
|
||||
light: paint.light,
|
||||
..*v
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let m = base
|
||||
* around(
|
||||
Mat4::from_rotation_y((-y_rot).to_radians()),
|
||||
@@ -1867,18 +1791,16 @@ mod tests {
|
||||
(
|
||||
name.to_owned(),
|
||||
Rect {
|
||||
u0: 0.25,
|
||||
v0: 0.125,
|
||||
u1: 0.375,
|
||||
v1: 0.25,
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let cache_open = std::cell::Cell::new(0.0);
|
||||
let cache_lid = |_| cache_open.get();
|
||||
let mut scene = Scene {
|
||||
camera: DVec3::ZERO,
|
||||
orientation: Quat::IDENTITY,
|
||||
@@ -1895,61 +1817,11 @@ mod tests {
|
||||
banner: &|_| Vec::new(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
};
|
||||
// Compare the cached world path with the original model renderer,
|
||||
// including atlas replacement, current light and animated fallback.
|
||||
scene.lid = &cache_lid;
|
||||
for nether in [false, true] {
|
||||
scene.nether = nether;
|
||||
for part in [ChestPart::Single, ChestPart::Left, ChestPart::Right] {
|
||||
for y_rot in [0.0, 90.0, 180.0, 270.0] {
|
||||
let kind = Kind::Chest {
|
||||
block: "chest".into(),
|
||||
texture: "entity/chest/normal",
|
||||
part,
|
||||
y_rot,
|
||||
double_with: None,
|
||||
};
|
||||
for (open, packed) in [(0.0, 0xF0), (0.0, 0x7F), (0.5, 0x7F)] {
|
||||
cache_open.set(open);
|
||||
let base = Mat4::from_translation(Vec3::new(3.25, -8.5, 12.75));
|
||||
let mut cached = Vec::new();
|
||||
let mut original = Vec::new();
|
||||
for (use_cache, out) in [(true, &mut cached), (false, &mut original)] {
|
||||
renderer.render_one(
|
||||
&scene,
|
||||
[0; 3],
|
||||
&kind,
|
||||
base,
|
||||
packed,
|
||||
diffuse_lights(nether),
|
||||
false,
|
||||
use_cache,
|
||||
out,
|
||||
);
|
||||
}
|
||||
assert!(!cached.is_empty());
|
||||
assert_eq!(cached.len(), original.len());
|
||||
for (a, b) in cached.iter().zip(&original) {
|
||||
assert!(
|
||||
(Vec3::from_array(a.pos) - Vec3::from_array(b.pos)).length() < 3e-6
|
||||
);
|
||||
assert_eq!(a.uv, b.uv);
|
||||
assert_eq!(a.color, b.color);
|
||||
assert_eq!(a.light, b.light);
|
||||
assert_eq!(a.normal, b.normal);
|
||||
assert_eq!(a.layer, b.layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.nether = false;
|
||||
scene.lid = &|_| 0.0;
|
||||
for state in 0..names.len() as u32 {
|
||||
let mut base = Vec::new();
|
||||
renderer.render_block(&scene, [0; 3], state, Mat4::IDENTITY, 0xff, &mut base);
|
||||
let mut cracks = Vec::new();
|
||||
renderer.render_breaking(&scene, [0; 3], state, [0.0; 2], &mut cracks);
|
||||
renderer.render_breaking(&scene, [0; 3], state, &mut cracks);
|
||||
assert!(!cracks.is_empty());
|
||||
assert_eq!(
|
||||
base.iter().map(|v| v.pos).collect::<Vec<_>>(),
|
||||
@@ -1963,7 +1835,7 @@ mod tests {
|
||||
scene.lid = &|_| 1.0;
|
||||
}
|
||||
let mut animated = Vec::new();
|
||||
renderer.render_breaking(&scene, [0; 3], state, [0.0; 2], &mut animated);
|
||||
renderer.render_breaking(&scene, [0; 3], state, &mut animated);
|
||||
assert!(cracks.iter().zip(&animated).any(|(a, b)| a.pos != b.pos));
|
||||
scene.game_time = 0;
|
||||
scene.lid = &|_| 0.0;
|
||||
|
||||
@@ -23,21 +23,7 @@ pub fn decal_uv([x, y, z]: [f32; 3], direction: Direction) -> [f32; 2] {
|
||||
[-p[0], -p[1]]
|
||||
}
|
||||
|
||||
/// The light bytes of a crack vertex: the pass is unlit, so they carry the origin of the
|
||||
/// vertex's destroy stage sprite (each crack in `LevelRenderer.destroyingBlocks` has its own
|
||||
/// stage; every stage sprite has the same size, `crumbling_sprite.zw`).
|
||||
pub fn stage_light([u0, v0]: [f32; 2]) -> u32 {
|
||||
let q = |x: f32| (x.clamp(0.0, 1.0) * 65535.0).round() as u32;
|
||||
q(u0) | (q(v0) << 16)
|
||||
}
|
||||
|
||||
pub fn geometry(
|
||||
model: &BakedModel,
|
||||
origin: glam::Vec3,
|
||||
translucent: bool,
|
||||
stage: [f32; 2],
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
pub fn geometry(model: &BakedModel, origin: glam::Vec3, translucent: bool, out: &mut Vec<Vertex>) {
|
||||
// Vanilla visits all six cull groups and then the unculled faces, even beside solid neighbours.
|
||||
for cull in Direction::ALL.map(Some).into_iter().chain([None]) {
|
||||
for face in model.faces.iter().filter(|face| face.quad.cull == cull) {
|
||||
@@ -47,7 +33,7 @@ pub fn geometry(
|
||||
uv: decal_uv(pos, face.quad.facing),
|
||||
layer: if translucent { 12 } else { 11 },
|
||||
color: 0xffff_ffff,
|
||||
light: stage_light(stage),
|
||||
light: 240 | (240 << 8),
|
||||
normal: [0; 4],
|
||||
});
|
||||
}
|
||||
@@ -56,7 +42,7 @@ pub fn geometry(
|
||||
}
|
||||
|
||||
/// Project a block entity's transformed quads onto the same destroy-stage pass.
|
||||
pub fn entity_geometry(vertices: &mut [Vertex], origin: glam::Vec3, stage: [f32; 2]) {
|
||||
pub fn entity_geometry(vertices: &mut [Vertex], origin: glam::Vec3) {
|
||||
for quad in vertices.as_chunks_mut::<4>().0 {
|
||||
let positions = [0, 1, 2].map(|i| glam::Vec3::from(quad[i].pos));
|
||||
let normal = (positions[1] - positions[0]).cross(positions[2] - positions[0]);
|
||||
@@ -79,7 +65,7 @@ pub fn entity_geometry(vertices: &mut [Vertex], origin: glam::Vec3, stage: [f32;
|
||||
vertex.uv = decal_uv((glam::Vec3::from(vertex.pos) - origin).to_array(), facing);
|
||||
vertex.layer = 11;
|
||||
vertex.color = 0xffff_ffff;
|
||||
vertex.light = stage_light(stage);
|
||||
vertex.light = 240 | (240 << 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,24 +73,6 @@ pub fn entity_geometry(vertices: &mut [Vertex], origin: glam::Vec3, stage: [f32;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn stage_origin_survives_the_light_bytes() {
|
||||
// entity.wgsl's `wrap`: the low and high 16 bits over 65535.
|
||||
for origin in [[0.0, 0.0], [0.5, 0.25], [0.123_456, 0.987_654], [1.0, 1.0]] {
|
||||
let light = stage_light(origin);
|
||||
let back = [
|
||||
(light & 0xffff) as f32 / 65535.0,
|
||||
(light >> 16) as f32 / 65535.0,
|
||||
];
|
||||
for k in 0..2 {
|
||||
assert!(
|
||||
(back[k] - origin[k]).abs() <= 0.5 / 65535.0,
|
||||
"{origin:?} -> {back:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::models::catalog::BakedFace;
|
||||
use horizon_assets::{bake::ModelQuad, model::Material};
|
||||
fn slab_top() -> BakedModel {
|
||||
@@ -141,13 +109,7 @@ mod tests {
|
||||
fn slab_overlay_follows_baked_top_not_unit_cube() {
|
||||
let model = slab_top();
|
||||
let mut vertices = Vec::new();
|
||||
geometry(
|
||||
&model,
|
||||
glam::Vec3::new(2.0, 3.0, 4.0),
|
||||
false,
|
||||
[0.0; 2],
|
||||
&mut vertices,
|
||||
);
|
||||
geometry(&model, glam::Vec3::new(2.0, 3.0, 4.0), false, &mut vertices);
|
||||
assert_eq!(vertices.len(), 4);
|
||||
for (v, p) in vertices.iter().zip(model.faces[0].quad.positions) {
|
||||
assert_eq!(v.pos, [p[0] + 2.0, p[1] + 3.0, p[2] + 4.0]);
|
||||
@@ -158,14 +120,14 @@ mod tests {
|
||||
fn entity_overlay_uses_block_local_projected_uvs() {
|
||||
let origin = glam::Vec3::new(12.0, -3.0, 8.0);
|
||||
let mut expected = Vec::new();
|
||||
geometry(&slab_top(), origin, false, [0.0; 2], &mut expected);
|
||||
geometry(&slab_top(), origin, false, &mut expected);
|
||||
let mut actual = expected.clone();
|
||||
for vertex in &mut actual {
|
||||
vertex.uv = [0.0; 2];
|
||||
vertex.layer = 0;
|
||||
vertex.color = 0;
|
||||
}
|
||||
entity_geometry(&mut actual, origin, [0.0; 2]);
|
||||
entity_geometry(&mut actual, origin);
|
||||
assert_eq!(
|
||||
bytemuck::cast_slice::<_, u8>(&actual),
|
||||
bytemuck::cast_slice::<_, u8>(&expected)
|
||||
@@ -181,7 +143,7 @@ mod tests {
|
||||
#[test]
|
||||
fn translucent_overlay_has_its_own_pass() {
|
||||
let mut vertices = Vec::new();
|
||||
geometry(&slab_top(), glam::Vec3::ZERO, true, [0.0; 2], &mut vertices);
|
||||
geometry(&slab_top(), glam::Vec3::ZERO, true, &mut vertices);
|
||||
assert!(vertices.iter().all(|v| v.layer == 12));
|
||||
}
|
||||
#[test]
|
||||
@@ -189,7 +151,7 @@ mod tests {
|
||||
let mut model = slab_top();
|
||||
model.faces.clear();
|
||||
let mut vertices = Vec::new();
|
||||
geometry(&model, glam::Vec3::ZERO, false, [0.0; 2], &mut vertices);
|
||||
geometry(&model, glam::Vec3::ZERO, false, &mut vertices);
|
||||
assert!(vertices.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -386,12 +386,6 @@ pub struct ChatScreen {
|
||||
parse: Option<ParseResults>,
|
||||
/// The completions at the cursor (`pendingSuggestions`): range start (bytes) and list.
|
||||
pending: Option<(usize, Vec<Suggestion>)>,
|
||||
/// Completions waiting for the server's (an `ask_server` node at the
|
||||
/// cursor): `pendingSuggestions` is not done until they come.
|
||||
awaiting: Option<Awaiting>,
|
||||
/// The request to send (`ServerboundCommandSuggestionPacket`): id and
|
||||
/// the command up to the cursor.
|
||||
request: Option<(i32, String)>,
|
||||
suggestions: Option<SuggestionList>,
|
||||
usage: Vec<Text>,
|
||||
usage_position: i32,
|
||||
@@ -400,16 +394,6 @@ pub struct ChatScreen {
|
||||
keep_suggestions: bool,
|
||||
pub auto_suggestions: bool,
|
||||
}
|
||||
/// Local completions waiting for the server's reply to request `id`.
|
||||
struct Awaiting {
|
||||
id: i32,
|
||||
/// The command up to the cursor, as sent.
|
||||
truncated: String,
|
||||
local: Vec<Suggestion>,
|
||||
}
|
||||
/// `ClientSuggestionProvider.pendingSuggestionsId`: one counter for the
|
||||
/// connection, so a reply to an older request never matches.
|
||||
static SUGGESTION_ID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
/// `CommandSuggestions`' fill colour.
|
||||
const FILL: i32 = -805306368;
|
||||
fn byte_at(s: &str, chars: usize) -> usize {
|
||||
@@ -458,8 +442,6 @@ impl ChatScreen {
|
||||
history_pos: history_len,
|
||||
parse: None,
|
||||
pending: None,
|
||||
awaiting: None,
|
||||
request: None,
|
||||
suggestions: None,
|
||||
usage: Vec::new(),
|
||||
usage_position: 0,
|
||||
@@ -536,84 +518,24 @@ impl ChatScreen {
|
||||
}
|
||||
if cursor >= 1 && (self.suggestions.is_none() || !self.keep_suggestions) {
|
||||
let parse = self.parse.as_ref().expect("parsed above");
|
||||
let (range, list, ask) =
|
||||
c.dispatcher
|
||||
.client_suggestions_asking(parse, cursor_b, c.registries);
|
||||
if ask {
|
||||
// ClientSuggestionProvider.customSuggestion: the
|
||||
// suggestions are done when the server answers.
|
||||
let id = SUGGESTION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let truncated = command[..cursor_b].to_owned();
|
||||
self.request = Some((id, truncated.clone()));
|
||||
self.awaiting = Some(Awaiting {
|
||||
id,
|
||||
truncated,
|
||||
local: list,
|
||||
});
|
||||
self.pending = None;
|
||||
} else {
|
||||
self.awaiting = None;
|
||||
self.pending = Some((range.0, list));
|
||||
self.update_usage_info(m, c);
|
||||
}
|
||||
let (range, list) = c
|
||||
.dispatcher
|
||||
.client_suggestions(parse, cursor_b, c.registries);
|
||||
self.pending = Some((range.0, list));
|
||||
self.update_usage_info(m, c);
|
||||
}
|
||||
} else if !command.chars().all(char::is_whitespace) {
|
||||
let partial = &command[..cursor_b];
|
||||
let mut builder = SuggestionsBuilder::new(partial, last_word_index(partial));
|
||||
builder.suggest_matching(c.registries.players.iter().map(String::as_str));
|
||||
let (range, list) = merge_suggestions(partial, builder.out);
|
||||
self.awaiting = None;
|
||||
self.pending = Some((range.0, list));
|
||||
self.usage_width = 0;
|
||||
self.usage_position = 0;
|
||||
} else {
|
||||
self.awaiting = None;
|
||||
self.pending = None;
|
||||
}
|
||||
}
|
||||
/// The completion request to send to the server, if one is waiting.
|
||||
pub fn take_request(&mut self) -> Option<(i32, String)> {
|
||||
self.request.take()
|
||||
}
|
||||
/// `ClientSuggestionProvider.completeCustomSuggestions`: the server's
|
||||
/// completions for request `id`, with `range` in UTF-16 units of the
|
||||
/// command sent. They join the local ones (`Suggestions.merge`) and the
|
||||
/// pending suggestions are done.
|
||||
pub fn server_suggestions(
|
||||
&mut self,
|
||||
m: &Measure,
|
||||
c: &Commands,
|
||||
id: i32,
|
||||
range: (usize, usize),
|
||||
list: Vec<(String, Option<String>)>,
|
||||
) {
|
||||
let Some(awaiting) = self.awaiting.take_if(|a| a.id == id) else {
|
||||
return;
|
||||
};
|
||||
let input = &awaiting.truncated;
|
||||
let byte = |units: usize| {
|
||||
let mut n = 0;
|
||||
input
|
||||
.char_indices()
|
||||
.find(|(_, ch)| {
|
||||
let past = n >= units;
|
||||
n += ch.len_utf16();
|
||||
past
|
||||
})
|
||||
.map_or(input.len(), |(i, _)| i)
|
||||
};
|
||||
let range = (byte(range.0), byte(range.0 + range.1));
|
||||
let mut all = awaiting.local;
|
||||
all.extend(list.into_iter().map(|(text, tooltip)| Suggestion {
|
||||
range,
|
||||
text,
|
||||
tooltip,
|
||||
int_value: None,
|
||||
}));
|
||||
let (range, list) = merge_suggestions(input, all);
|
||||
self.pending = Some((range.0, list));
|
||||
self.update_usage_info(m, c);
|
||||
}
|
||||
/// `CommandSuggestions.updateUsageInfo`.
|
||||
fn update_usage_info(&mut self, m: &Measure, c: &Commands) {
|
||||
let Some(parse) = &self.parse else { return };
|
||||
|
||||
Loaded 100 of 1091 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user