ditto without --norsrc --noextattr --noqtn stores an AppleDouble ._ entry
beside every file that has extended attributes, so the macOS release zip
held hundreds of them (horizon-macos-arm64/._horizon-ctl, tools/._*.py).
Package it as packaging/macos/package.sh already does, and fail the job if
any ._ or __MACOSX entry is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LAUNCHER_PATHS and launcher.yml's push paths now list horizon-bench,
horizon-friends, horizon-mods and horizon-port-pack too, the launcher's
path packages per cargo metadata. A change there (like the horizon-mods
dependency that left launcher/Cargo.lock stale) then builds and releases
the launcher instead of going unnoticed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the launcher links another launcher's client jar with a hard link
(a copy across volumes) and its libraries and assets with a junction, since
symlinks need developer mode. canonicalize() of a hard link is its own path,
so the sources tests, which compared canonical paths, failed on Windows and
blocked every launcher release since launcher-v0.4.0. They now check that the
linked jar is the launcher's file (a byte written through one shows in the
other), which holds for a symlink and a hard link alike.
The modpack profile test compares paths as paths rather than strings ending
in "a/carry".
mklink /J's paths are always quoted: Rust only quotes arguments with spaces,
and cmd reads an unquoted & or ^ in a user folder as its own syntax.
CI runs every launcher test target, not just the library's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
world_spawn now takes about 1480 s on a loaded Mac mini runner, just
under the old 1500 s limit, so PRs failed on a timeout rather than a
mismatch (#567, #514).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- packaging/server/install.sh (Linux, macOS) and install.ps1 (Windows) install the latest release (or
--version) from GitHub, check the SHA-256 GitHub publishes for it, and keep releases side by side:
<dir>/current points at the one in use, so an update or a rollback is one link, and the game files
prepared from the owner's Minecraft stay in <dir>/game-files across updates.
- packaging/server/Dockerfile builds the server and the engine; the prepared game files are mounted
read-only, and the first start writes server.toml from HORIZON_EULA, HORIZON_WORLD, HORIZON_SEED,
HORIZON_PORT, HORIZON_WHITELIST, HORIZON_PLAYERS and HORIZON_OP (`init` reads them too).
- `horizon-server service install|uninstall|print` writes and loads a systemd unit (user, or --system),
a launchd agent, or a Windows task at logon. It runs the installer's launcher, so updates apply on
the next restart.
- Releases build horizon-server and ship it beside horizon-ctl and horizon-prepare; PLAY.txt says how
to run a server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The macOS release build has failed since 05:54 UTC on 2026-10-16: on the
self-hosted Mac mini runner the game drew its 1500 frames in about 2.5 s,
before the world had started generating (chunks 0, player null), so no
release has been published since v2026.10.16.03.
--world-frames N exits after N frames drawn with the player in the world.
It is otherwise a frame-limited run (classic menus, --timeout-seconds), and
--frames keeps its meaning for the harnesses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- First run (and when CONSENT_VERSION changes): a question over the launcher
with four switches, all off, what each sends in plain words, what is never
sent, "View what would be sent" (the exact JSON), and "Send nothing" /
"Save choices".
- Settings → Data collection: the same switches any time, the install ID and
what waits, "View what would be sent" and "Delete my data" (server deletion,
outbox cleared, new install ID).
- The outbox is sent at start and on close (at most 3 s), only when there is a
server (none yet) and only what the player allows.
- CLI `horizon-launcher data status|allow|deny|show|delete|send|bug|benchmark`
and MCP tools data_collection, set_data_collection, data_preview,
delete_my_data, report_bug. Bug reports and benchmark uploads are printed for
the player to confirm; logs are scrubbed and lose chat lines.
- horizon-telemetry joins the launcher's paths (launcher.yml, release_train,
pr_ready).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manifest and .hmod reading, capability checks against the component's imports,
fuel and memory limits per call, a trapping mod stopped alone, hot reload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changes to horizon-core or horizon-world make ~280-300 harnesses rerun, which takes
60-90 min on the Mac mini runners; at 45 min those runs were cancelled and proved nothing
(#269, #280, #292 today).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/horizon-telemetry-server, its own workspace like the launcher's (the
game's workspace never builds a web server):
- POST /v1/batch (zstd, decoded and checked by horizon-telemetry), DELETE
/v1/installs/<id>, GET /v1/aggregate (cached 10 min, CORS for the website),
GET /healthz.
- SQLite: machine descriptions stored once, one row of columns per session
with 80-byte summary blobs, zstd bodies for crash/bug/benchmark, daily totals
rebuilt when a day changes and kept after raw rows expire (180 days). About
190 bytes per session on disk.
- Per-address token bucket (addresses in memory only), per-install daily cap,
groups under 5 install-days hidden from the totals.
- Tests: refusals, rate limit, daily cap, deletion (rows and totals), retention,
storage size, and end to end with the launcher's uploader.
- Dockerfile and a path-filtered CI workflow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pr_ready is the one self-check to run before asking for review: from the
files a branch changes it runs rustfmt, clippy with warnings denied, the
tests of the touched crates (mapped through cargo metadata), the launcher's
own workspace when it is affected, the tokens.css check, the Cargo.lock
rule, the Python tests and the public-tree check, and the vanilla harnesses
the change can affect, picked by tools/parity.py exactly as CI's parity job
picks them. It runs locally or on a fleet machine through ~/fleet/rbuild;
on the Mac mini the harnesses use the parity runner's own workspace and
scripts. It prints one Markdown block for the pull request body and exits
non-zero on any failure.
tools/parity.py gains --changed-files for copies without Git history, and
tools/ci/vanilla-data.sh accepts the preparation key from outside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Releases now ship as often as changes land, so nothing they leave behind may
grow without bound. Workflow uploads keep 3 days (nightly parity logs 7); the
releases carry their own assets.
tools/ci/runner-gc.sh, run every 6 hours by launchd on the Mac mini from a copy
of main, skips a runner in a job, removes a checkout's cargo build folders past
12 GB and harness work folders under .local/ untouched for 3 days, and keeps the
2 newest shared preparations while no runner works. The runners build through
the machine's sccache, so what it removes comes back from the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ludovic wants releases as often as changes land. A kind now releases when
player-facing pull requests merged since its last release and the newest of
them merged 15 minutes ago (a burst of merges becomes one release), or the
oldest an hour ago when merges never pause; release-now still ships at once.
There is no minimum time between releases. The scheduled checks run every 10
minutes (game at :02, launcher at :07) so a settled release goes out without
another merge.
Release titles keep proper nouns (Horizon's mark, Microsoft, LAN, ...) when
they lowercase the clauses after the first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS: packaging/macos/package.sh (run by launcher.yml) builds Horizon.app (Info.plist, bundle
id dev.horizon.launcher, macOS 11+, the .icns), signs it ad hoc, and makes the two release
assets: horizon-launcher-macos-arm64.dmg, the installer (the app and an Applications link over
a background, laid out by dmgbuild without a GUI), and the .zip the launcher updates from. A
launcher inside the app replaces the whole bundle: copied beside it with ditto, swapped in by
renames, restarted; a bare binary, as older launchers are, still finds its binary in the zip.
A macOS test zips, unpacks, swaps and re-verifies a signed bundle. Signing for real is a
MACOS_SIGN_IDENTITY away (packaging/macos/README.md, with what Gatekeeper asks meanwhile).
Linux: release builds keep horizon-launcher.desktop (with the binary's current path),
horizon.desktop (hidden, for the game window's icon) and the hicolor icons under
$XDG_DATA_HOME, without root; `horizon-launcher desktop-entry --remove` takes them out and
stops it, `desktop-entry` puts them back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nightly.yml runs every harness from scratch on main at 03:00 Paris time on the Mac mini's runner, after
preparing the resources again if the preparer changed. tools/ci/parity_issues.py compares the results with
the previous audit: a harness that passed and now fails gets one issue labelled parity-regression (an open
one is commented on), and an issue closes when its harness passes again. The bugs session picks them up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
launcher.yml releases the launcher by itself: after a push to main that changes launcher/,
crates/horizon-prepare or crates/horizon-theme, hourly, and by hand, with the game's rules
(tools/release_train.py decide --kind launcher: player-facing pull requests and a 3-hour cooldown,
release-now, skip-release, RELEASES_PAUSED). launcher-vX.Y.Z is the next patch, or the next minor after a
pull request labelled feature. Linux on ubuntu-22.04, Windows on GitHub, macOS on the Mac mini's runner;
published with --latest=false.
No version-bump commits: VERSION reads HORIZON_LAUNCHER_VERSION with option_env!, falling back to
Cargo.toml's, and --version, the User-Agent and the self-update comparison use it; each build checks
--version says the release's version.
With several game releases a day, the newest launcher release soon falls out of the newest 100 (launcher)
or 30 (website) releases: both now look the newest launcher-v tags up one by one when the list has none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked on the Mac mini: 1500 offscreen frames in 12 s, 81 chunks generated, 4153 sections meshed, a player.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.yml now releases the game by itself: after every green checks run of a push to main, every hour
(a release the cooldown held goes out without another merge) and by hand. tools/release_train.py decides
from the pull requests merged since the last published game release: a release when one changed what
players run and the last release is 3 hours old, at once for a pull request labelled release-now;
skip-release pull requests don't count. RELEASES_PAUSED=1 stops it. Tags vYYYY.MM.DD, then .1, .2 the
same UTC day.
Linux (ubuntu-22.04) and Windows build on GitHub with Cargo caches, macOS on the Mac mini's runner
(MAC_RUNNER=macos-latest sends it to GitHub). Each builds horizon-client, horizon-ctl and horizon-prepare
(all three named: --bin selects only the named binaries), checks all three exist, checks their libraries,
starts each one, packages with xtask and archives; the Mac also runs the game offscreen for 300 frames on
a new world with resources prepared from its own Minecraft. The notes: New (label feature), Fixed (the
rest), Known issues (docs/known-issues.md). A hand-started run off main, or with dry_run, builds
everything and writes the notes but publishes nothing.
docs/release-flow.md describes the flow, the labels, pausing and rolling back; the launcher gets a test
that a release turned back into a draft is never offered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify keeps its dependencies and workspace build in a cache saved by main's runs (Swatinem/rust-cache).
The new parity job runs on the Mac mini's self-hosted runner (label horizon-game), which holds the
maintainer's own Minecraft files: tools/ci/vanilla-data.sh links .local/ to the runner's copy and prepares
the resources with the pull request's own horizon-prepare, once per preparer version; then
tools/parity.py --changed <base> runs only the harnesses whose traced dependencies changed since the merge
base and that have no pass for their current inputs. A fork's pull request runs it only when a maintainer
adds run-parity, once per label, on the commit it had then.
parity.py: --changed REF, and linux_only in tools/parity_hosts.json for harnesses that differ on other
platforms for a reason that is not a Horizon bug (skipped there with the reason, unless named).
docs/release-flow.md starts with the pull request checks and merging.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --bin flags, cargo builds only the named binaries of every selected
package, so -p horizon-prepare built nothing and the import checks failed on
target/release/horizon-prepare (every run since #92).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cargo run -p xtask -- package BINARY OUTDIR` does what package.sh did:
OUTDIR wiped and recreated, the `git archive HEAD tools
mods/horizon/src/test/java crates/horizon-prepare research-lock.json LICENSE`
tree extracted (the tar read in Rust, executables kept executable), the client,
horizon-ctl and horizon-prepare copied (.exe beside an .exe, the same error when
one is missing) and PLAY.txt. release.yml calls it; package.sh is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vanilla 26.3 has no gamepad input, so this is Horizon's own, on Bedrock's
default layout (docs/controllers.md). gilrs reads the pads; the pure part
(bindings, radial deadzones, the look curve, button edges, the
horizon-controller.txt settings file) is in gamepad.rs with unit tests.
Buttons act as the vanilla key mapping they're bound to, through the same
path a rebound keyboard key takes (the keyboard's post-rebinding handling
moves into Game::mapped_key). The left stick is an analog move vector,
the right stick turns the view per frame, LB/RB step the hotbar, B and
Start leave screens and A respawns. horizon-ctl pad injects a virtual pad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher prepares a version in-process when it ships the preparer sources
the launcher was built with, with the version's own horizon-prepare when they
differ, and with the Python script only for versions made before it. Release
folders ship horizon-prepare and its sources; resources are reused between
versions with the same preparer. The game's messages, PLAY.txt, the docs and
release builds name horizon-prepare, and tools/prepare_native_client.py is a
wrapper of it for the development tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.yml builds horizon-ctl beside the client (checked for system-only
imports like it), packaging/package.sh copies it from beside the client binary
and refuses to package without it (the macOS folders made by hand go through
the same script), and package_native_client.py adds it to private archives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared CI runners hit GitHub's anonymous rate limit in the smoke test (launcher 0.1.2's
macOS build failed twice on it). Players never set the variable, so nothing changes for them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- links.json on main gives the Discord and sponsor links (built-in fallback offline,
empty hides the button); 'links' command and MCP tool.
- Install new versions automatically is on unless explicitly turned off (new settings
key, so 0.1.1's written default does not count as a choice).
- Settings: Horizon / Classic menu style, the horizon.txt menuStyle line the game reads;
'menu-style' command and MCP tool.
- launcher workflow: install latest into a temp home, check imports (Windows, Linux client)
and that the client starts, on all three platforms; also runs on launcher-* branches.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Windows launcher was built without crt-static and imported VCRUNTIME140.dll,
which only the Visual C++ redistributable installs: players without it got a
missing-DLL dialog before anything started. The game itself links only system
DLLs (vulkan-1.dll is loaded at run time).
- launcher.yml links the C runtime on Windows.
- tools/check_imports.py reads PE import and delay-import tables (and ELF
loader, needed libraries and run paths); release.yml and launcher.yml fail
when a Windows binary imports a non-system DLL.
- release.yml builds Linux on Ubuntu 22.04: the NixOS-made Linux release asked
for a /nix/store loader, so it ran nowhere but NixOS.
- No adapter: the game says the Vulkan driver is missing and what to install,
and on Windows keeps the console open until Enter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removed (each unreferenced by code, workflows, tests or living docs; no harness
dependency record names any of them, and parity.py --list still finds all 281):
- 22 one-off write-ups of the Java-pack experiments in docs/: adaptive-terrain,
client-graphics-smoke, compact-headers, core-performance, culling-empty,
culling-rays, debounce-queue, dispatch-retry, foreground-first,
graphics-api-comparison, heap-budget, mac-first-test, optimization-plan,
playable-validation, quad-reuse, server-graphics-context, shader-world-smoke,
terrain-measurements, terrain-position, terrain-read-wait, tick-membership,
worker-pacing. Their findings are folded into one paragraph of java-packs.md.
- 24 benchmarks/ observation dumps only those write-ups linked (client graphics,
culling, queues, heap, Mac Vulkan, shader worlds, software identity, and the
unlinked rust-core, rust-worldgen-2026-10-05, rust-native-client and
rust-cuboid-model-decoding runs), plus 12 unlinked files inside kept
report folders. The scoped-parity reports the living docs cite stay.
- debug/: one server log from a chunk investigation.
- The manual macos-shader-research and macos-vulkan-research workflows.
- tools/logs/latest.log, an empty log committed by accident (now ignored).
Kept: research-lock.json (parity.py, packaging, the launcher and datafix read
it), the Java-pack how-tos (vulkan, collectors, profiling, process-memory,
tick-state) and tools/test_client_graphics.py (ci.yml runs it).
README: leads with what Horizon is and "Download the launcher" (the
launcher-v* releases; the launcher updates itself), then build from source
for developers. Direct game downloads and PLAY.txt steps for players are gone;
the launcher has its own section with two screenshots. The roadmap merges the
known differences, the final checks and the plans after parity, under the
"Roadmap" heading the launcher's What's next page reads. The parity table and
its 100%-of-what-the-harnesses-compare note are unchanged.
docs/README.md indexes what remains: build and run, parity (status and
workflow), world generation, world data and simulation, the old Java track.
Links to the removed pages and README anchors are fixed.
packaging/PLAY.txt now speaks to players who came through the launcher and
points anyone with a bare archive to it, keeping the manual steps short.
The launcher's README link points at main instead of the launcher branch.
.gitignore: tools/logs/ and .DS_Store. No code behaviour changes; the compiler
and clippy flag no unused code and the tree has no stale TODOs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control socket reports itself unavailable on Windows, which has no
Unix domain sockets, and horizon-ctl uses the same stream type. A release
workflow builds the Windows client by hand; packaging/package.sh assembles
the binary, the resource preparation tool and PLAY.txt, without any of
Minecraft's files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Horizon no longer fetches anything of Minecraft from Mojang's servers. The
client jar, version JSON, libraries and assets come from the user's official
launcher install, and the server jar is one they downloaded themselves
(--server-jar or HORIZON_SERVER_JAR). Every file is checked against checksums
pinned in research-lock.json, and the prepared .local/ layout is byte-identical
to the old downloads, so the harnesses and the native client are unchanged.
CI keeps the offline checks on GitHub's runners; the vanilla comparisons and
the mod build need the game, so they move to a manual job on a self-hosted
runner that holds the maintainer's own copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everyday work is built and tested on Linux only. macOS and Windows jobs run only when started by hand (workflow_dispatch: gh workflow run <workflow> --ref <branch>), not on pull requests or pushes to main. Release workflows are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Horizon now targets Linux and macOS only: remove the Windows CI runners and
wgpu's DX12 backend, and update the scope docs. The Ubuntu RTX 4080 desktop
becomes the first physical Linux target. tools/package_native_client.py bundles
a release build with its prepared resources and a launcher for playing on
another machine of the same platform.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>