- 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>
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>
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>
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>
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>
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>