Every function the game offers mods has the API level it arrived at
(crates/horizon-mods/src/api_level.rs): level 0 is the contract of
Horizon 0.2.67 and 0.2.68, level 1 adds render.set-minimap. A mod needs
the level of the newest function it imports, read from its component.
- The game refuses a mod that calls a function it doesn't have with
"made for a newer Horizon (it uses ...): update Horizon to run it"
instead of wasmtime's linking error.
- The registry index lists each mod version's level (api_level, read
from the file by CI); check_file compares it when the entry has one,
and the moved extras' installer takes the newest version this game
can run.
- cargo horizon-mod check prints the level.
- The api_reference test fails until a new WIT function has a level.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Share the asset index and verification state, stage checked imports while hashing, preserve indexed replacements, and clone independent resource copies where supported.
Co-Authored-By: Codex (GPT-6.1) <noreply@openai.com>
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
The launcher's Appearance setting darkened the page but not the window's native chrome: NSWindow keeps the system appearance, so a launcher set to Dark still drew a light title bar on a light desktop. apply_appearance (titlebar.rs) pins the NSWindow's appearance to the choice at startup and when the setting changes; System clears the pin so the chrome follows the computer again.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A browser source for Horizon's own registry (docs/api/mod-index.md): it reads index.json
(HORIZON_MOD_INDEX for a test registry, cached in the game folder for when the registry can't
be reached), searches it by kind and words, lists versions with the SHA-256 the index gives,
and blocks the versions it says were taken down. Downloads accept the registry's release host
and check SHA-256; file:// files only from a local index.
Installing, the takedown check before play and the CLI come next.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 Agent: merged after verify passed. Ludovic asked on 2026-10-23 for every hardware fact players send (opt-in). The end-to-end run against a local server is in the description: breakdowns need ≥3 installs, with no install ID or exact value. The consent version goes up to 2, so players who opted in are asked again, as the privacy page promises.
The launcher's core gains `browser`: search Modrinth for what Horizon loads
(resource packs, Iris/OptiFine shader packs, modpacks; never Java mod jars),
say how well each result runs, pick the newest version that runs, and
download a file only from Modrinth's or CurseForge's file servers, with a
plain file name, kept only when its size and SHA-512 (SHA-1 on CurseForge)
match. CurseForge is a pluggable source that stays off until an API key
exists (HORIZON_CURSEFORGE_API_KEY at run time or build time; the launcher
workflow passes the repository secret). docs/mod-browser.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.github/release-line.txt = 0.2: the release train's next runs publish v0.2.0 (a beta, as always)
and launcher@0.2.0 at once, "the first release of 0.2" (docs/versioning.md). Merge it only after
the bridges (the last dated game release, made stable, and launcher-v0.17.0) are out: the go-day
steps are in docs/versioning.md.
Also, because they say 0.2 is out: the site's FAQ explains the numbers, Install says an old launcher
updates twice, hosting.md gives the one command for a server that says it is up to date, and the
launcher's Cargo.toml says 0.2.0 so builds from source (packages) are on the 0.2 line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/gen_version_consts.py writes what the pinned client jar's version.json
states (name, id, protocol, data version, pack formats, Java, stable, build
time) into horizon_core::version, and every Rust site reads it from there:
PROTOCOL_VERSION, GAME_VERSION, DATA_VERSION, the save's and the datafixer's
version name, /version, the crash report, the legacy ping, the dedicated
server's start line, the title screens, the default resource folder, the
resource pack format, and the launcher's and horizon-ctl's fallbacks. CI
checks the file against the pinned jar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher (game installs and its own updates) and `horizon-server update` fetch a release's
SHA256SUMS and SHA256SUMS.sig, check the signature against the keys built in from
crates/horizon-release-sign/release-keys.txt and the archive against the signed SHA-256, and
refuse anything unsigned, signed for another tag or by another key, or that doesn't match, with a
message saying why. Merge only once release-keys.txt lists the key and a signed stable release
exists: until then every download is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
horizon-port-pack now depends on horizon-mods (1f1d5a5a), which the
launcher's own lock file didn't have, so every --locked build of the launcher
failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Modpacks page between Versions and Patch notes (docs/modpacks.md, the
working version's launcher step):
- Import a pack file (.mrpack, CurseForge zip) or an instance folder (Prism,
MultiMC, CurseForge): horizon-port-pack's import, inventory (Modrinth by
file hash) and report into ports/<slug>/ in the game's home.
- The coverage report: ported, partial, not yet and not needed, with every
mod's class, tier, licence and reason, filtered by status.
- Port: the portable mods' jars (content, data tier) the player's own way
(their folder, the pack's links, Modrinth's CDN; what only CurseForge has
needs its key and is said so), carried over for the selected version's
prepared tables; the pipeline's log shows while it runs.
- Play: the game as the pack's own profile, ports/<slug>/home/ (its saves,
options.txt copied from the player's the first time) with the carried-over
mods as dev mods. The session flow is shared with Play (start_client).
- The same from the command line: horizon-launcher modpack list | import |
port | play | remove.
Nothing is uploaded or shared; jars and ports stay in the game's home.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The official launcher crashes on some Linux systems (rdbt's Arch), and
Horizon only read its layout. Preparing now finds the pinned version in the
chosen folder (or a folder above it, so an instance folder works), then in
every launcher's default folder on Linux (Flatpak included), macOS and
Windows:
- the official launcher: versions/<v>/<v>.jar
- Prism Launcher, MultiMC: the jar their meta/net.minecraft/<v>.json names
(a Maven name in libraries/), libraries/com/mojang/minecraft/<v>/...
- the Modrinth App: meta/versions/<v>/<v>.jar, meta/libraries
- ATLauncher, GDLauncher (data/, Next's datastore/):
libraries/net/minecraft/client/<v>/client-<v>.jar
The client jar is checked against the lock's sha1. horizon-prepare reads the
official layout and pins the version JSON's own sha1, so files found
elsewhere are linked into that layout in the data folder
(linked-minecraft/), with the pinned version JSON from piston-meta (by its
sha1, checked) when the launcher keeps only its own format or a rewrite.
Nothing else is copied or downloaded. Every preparer, old Python ones
included, reads the linked folder as an official one.
When nothing is found, the error lists the chosen folder and every installed
launcher's, with what each lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each session rewrites benchmarks/launcher-<time>-session.json in the data
folder (report format v1): start to first frame, release list fetch,
install/update and prepare durations, Play to the game's first frame (the
game now logs its first frame), RSS, UI frame times, binary size.
horizon-launcher benchmark starts the launcher N times (cold, then warm),
switches pages, measures idle CPU and memory and the release list fetch,
and writes one report with means and 95% CIs. Nothing is uploaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game joins `friend:CODE` addresses: the network thread resolves the code
through the relay, agrees on the key, settles on a direct or relayed stream
and connects the client to the tunnel's loopback port; everything after is
an ordinary join. Errors (unknown or ended code, wrong secret, host not
answering, version mismatch, rate limits) end on the Disconnected screen in
plain words. Shown and logged addresses hide the code's secret half.
Horizon's multiplayer page gains "Join a friend" (a code field); the classic
screens keep vanilla's buttons. The launcher's Play page gains a "Join a
friend" row (paste works) that starts the game straight into the join;
`horizon-launcher play --join CODE` does the same.
tools/test_friends.py runs a relay, a host Horizon sharing a world and a
guest Horizon, over plain LAN (--baseline), the direct path and the forced
relay path: chat, movement and blocks both ways, then the ended invite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A top bar (the mark, Horizon, the pages as text) replaces the sidebar; a mono footer carries
the launcher's version, its update and the community links. Every page is a display title, a
short muted line, then an index list: Play's resources and folders, the versions with their
names large and their size and state in mono, the patch notes, the settings. Buttons are pills,
the main action is the one violet thing on the screen, selection is an ink pill, the console,
notices and the crash report sit on soft plates without borders. The Play page ends with the
faint wordmark, half set below the window's edge.
The sunset photograph and its veils are gone (and the `image` dependency with them): the
window is the page colour, light or dark with the system. The opening draws the wordmark
letter by letter, then the launcher rises into place. HORIZON_LAUNCHER_APPEARANCE=light|dark
overrides the system's appearance, for screenshots and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mark is an H whose crossbar is the horizon: a violet line edge to edge, the stems above it
and their reflection below at a third, on whole units of a 16-unit grid so it stays sharp at
16 px. docs/media/brand holds the SVG masters (square icon, macOS tile, avatars, the mark for
light and dark backgrounds), the two alternatives that were shown with it, the GitHub social
preview and the Discord banner, and a README of what goes where and how to upload what only
Ludovic's accounts can.
`cargo run -p xtask -- icons` renders the masters (rsvg-convert) and packs, in plain Rust:
packaging/icons/horizon.ico and horizon.icns, Linux's hicolor icons 16–512 and the scalable
SVG, the game's run-time window icon, the avatars and the website's favicons.
packaging/linux has .desktop entries for the launcher and the game.
On Windows, build.rs embeds the .ico as icon resource 1 in horizon-client (and its tools) and
horizon-launcher (winresource, a Windows-only build dependency); gpui loads that resource as
the launcher's window icon. The game window sets the icon itself (X11, Windows) and names
itself `horizon` on Wayland so the desktop entry matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Device-code OAuth, Xbox Live, XSTS, Minecraft login, ownership and profile;
secrets in the OS keychain (oo7 on Linux, keyring on macOS/Windows), accounts.json
without tokens; Settings account rows, 'Playing as' on the Play page; CLI
'account' and MCP account tools; the game gets a 0600 session file it deletes.
Client ID is configuration (HORIZON_MSA_CLIENT_ID), empty = not configured.
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>
After installing a version (CLI install and update, the window's automatic install, MCP),
when it is not prepared and another installed version with the same Minecraft version is,
the launcher copies that version's prepared resources. Players who auto-updated to
v2026.10.13.1 no longer prepare again, and can play it without their Minecraft folder.
The prepared output depends on the Horizon version as well as Minecraft's: the manifest
records the hashes of the vanilla exporter sources it was made with, and
prepare_native_client.py writes every file and the manifest's schema. So resources are reused
only when both versions have the same pinned jars in research-lock.json, the same
prepare_native_client.py and the same exporter sources the manifest lists. They are copied
(prepare rewrites files in place, so hard links would tie the versions together), into a
partial folder renamed when complete. install and the MCP tool say when it is prepared already.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
links.json gains a website key; the launcher shows a quiet Website link under
Join Discord and lists it in the links CLI command and MCP tool.
Co-Authored-By: Claude Sonnet 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>
One theme for the launcher and the game: crates/horizon-theme holds the
palette, type scale, spacing, radii, control sizes, motion and the bundled
fonts; launcher/src/theme.rs and the game's ui_theme both read it. ui_theme
ports the launcher's components (button with hover layer and press state,
sidebar list with the sliding selection, segmented control, switch, rows)
and draws Geist and Instrument Serif through the HUD atlas.
The title is laid out like the launcher (sidebar, page header, rows) over a
live backdrop: Horizon generates an ice-spikes landscape for a fixed seed,
orbits a camera over it with falling snow, then blurs and cold-grades the
frame (post.rs). The launcher's opening (wordmark, then the interface) plays
once per session and a click or key skips it. Menu style Horizon / Classic
lives in horizon.txt, never options.txt; Classic keeps vanilla's drawing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client ran in the version's own folder, so saves and options.txt would have
landed there and been deleted with the version, and the saves in
~/.local/share/horizon (where the release folders' horizon.sh keeps them) were
not seen. It now runs in HORIZON_HOME, else horizon in the platform's data
folder, with --resources pointing at the version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>