🤖 Agent: merged after verify and real GL runs on the ThinkPad (Intel HD 620, GL 3.3 with extensions hidden: title, server thread and in-frame render, 0 errors; screenshots match Vulkan). Released at once: any GL driver without compute shaders crashed at start without it.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
wgpu refuses to create a buffer with INDIRECT usage on a device without
DownlevelFlags::INDIRECT_EXECUTION (OpenGL before 4.3 without compute shaders), so the game
panicked creating 'Section draws'. Multi-draw now also needs indirect execution, and without it
the buffer has no INDIRECT usage: sections are drawn one direct draw each, as they already were
without INDIRECT_FIRST_INSTANCE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: merged after verify and a real OpenGL run on the desktop (Mesa, GL 4.6 and 3.3: no crash, animated sprites on the CPU below 4.3). Released at once on Ludovic's request.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
An OpenGL driver can list compute shaders among its extensions while wgpu writes GLSL of its core
version, which has none (seen under MESA_GL_VERSION_OVERRIDE=3.3: the pipeline failed and the game
panicked). The pipeline is now created inside error scopes, and a failure falls back to the CPU path
with one warning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify, a real OpenGL run (Mesa GL 4.6: title and a world rendered, no errors) and the backend/shader unit tests. Released at once on Ludovic's request (release-now): a player's Radeon HD 7400M could not start Horizon.
SpriteAnimator wrote every changed sprite with a compute pass, which OpenGL 3.3 to 4.2 has no
equivalent for in wgpu: creating the pipeline failed. Without compute shaders and storage, the same
jobs now run on the CPU with animate.wgsl's integer mix, and each level is copied into its layer
through the upload belt. The GPU test runs both paths against the same CPU reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu's GL backend picks a texture's target from its layer count when it creates it: one layer makes
a 2D texture and a multiple of six on a square texture a cube map, and neither can be bound as
terrain.wgsl's texture_2d_array. On GL, the 1-layer stand-in for unused sprite arrays gets two
layers and a sprite array of 1 or 6n layers one empty layer more. Other backends are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GLSL combines a texture with its sampler, and naga refuses an entry point that reads one texture
through two (ImageMultipleSamplers): the BLUR override left both branches in fs_screen and
fs_vignette, so creating the screen effect pipelines failed on wgpu's GL backend at startup. Blurred
sprites now have their own entry points (fs_screen_blur, fs_vignette_blur); each pipeline still
samples once per pixel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu's GL backend (WGL on Windows, EGL on Linux). Auto tries Vulkan, then DirectX 12 on Windows,
then OpenGL; graphicsApi:opengl (or HORIZON_GRAPHICS_API=opengl) picks it. A Radeon HD 7400M
(TeraScale 2: DirectX 11, OpenGL 4.x) could not start Horizon at all.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thank you @rdbtCVS! Merged after review (a surface failure now falls through to the next backend, as adapter and device failures do) and this head's verify. Follow-up: an OpenGL fallback for GPUs with neither Vulkan nor DirectX 12 builds on this.
🤖 Agent: thank you @socksprox! Merged after review (two follow-up commits on your branch: `ui` is opt-in through `experimental = ["ui"]`, and the Settings button now appears when a mod sets its page after loading), tests in game in-frame, on a server-thread window and as a LAN guest, and this head's verify.
has_settings was read once, when the mods loaded, so a mod that called
set-settings from a command or a tick never got its Settings button
until a reload. The Mods page's rows now follow each mod call's output.
Also clippy with --features mods: needless borrows of the atlas in
draw_mod_screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/api/stability.md: a new API starts experimental, usable only by
opting in with a manifest field naming it. Capability ui now needs
experimental = ["ui"] beside capabilities; without it the manifest is
refused with that line to add. A capability promoted later stays
accepted in the list, so such manifests keep loading.
The ui test guest opts in; stability.md gets ui's row; docs/mods.md
shows the key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained PotionContents on the wire (#1796) in the same two functions
this branch hands to the shared codec. Resolved by keeping the codec and
teaching it the potion: components_tag writes it by its built-in registry
name, and the remote's id lookup asks the built-in registries first, as a
hub's SyncData::id already does. The round-trip test now covers a splash
potion.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
Tests in the same binary spawn processes (tar, the engine); between fork and exec the child holds a copy
of every open descriptor, the lock's too, so relocking right after drop could see WouldBlock. Seen twice
on CI (#1833, #1895).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
Horizon already encoded stacks for the wire (lan::guest::stack_to_network,
what a Horizon server sends its clients: damage, enchantments, stored
enchantments, repair cost, custom name, dyed colour, trim, maps, charged
projectiles, books) and decoded them on the client (remote.rs's
protocol_components + item_data::decode_components, every component it has
a codec for). The new encode/apply pair covered six components and
duplicated both.
- The client's SetCreativeModeSlot stack goes through stack_to_network.
- The decoder moves into item_sync::from_network, used by the client for
what its server sends and by the server (Hub::stack_from_network) for a
creative client's slot, so the server now keeps every component the
client's decoder reads, not six.
- One round-trip test covers encode then decode with registries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
Keep surface creation errors in the backend failure list so Windows Auto can try DirectX 12 after Vulkan fails.
Co-Authored-By: OpenAI Codex <[email protected]>
The base name (Tipped Arrow, Potion, White Banner) matched where vanilla's
first tooltip line (Arrow of Poison, Water Bottle, Ominous Banner) doesn't:
the creative harness found extra results for 'e', 'potion', 'tipped' and
'white'. name_line builds the full hover name through display_get.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The creative inventory's search box is an append-only EditBox of 50
characters, but its editing was a plain String pop, so Ctrl+Backspace
removed one character instead of the word before the caret, unlike every
other text field and vanilla's EditBox.keyPressed.
Move EditBox.getWordPosition and deleteWords into one shared helper in
ui.rs (the UI and chat text fields had two copies of it), and route a
Ctrl+Backspace in the creative search through it. A Select All selection
is still what backspace deletes first, as in vanilla.
Fixes#1764.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash Free) <[email protected]>
Name tags and styled HUD text drew a span's § codes as glyphs instead of
restyling what follows, as StringDecomposer.iterateFormatted does. A team
prefix like "§c" showed literally over players' heads, and a resource
pack's styled menu.disconnect kept its codes on the leave-server button.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Agent: merged after verify; Ludovic's yes/no on the 54 candidates from the project's history (roadmap check, 20 roadmap tests, 31 website tests, site builds with 90 items).
🤖 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.
Adds the ideas he said yes to, as roadmap items with plain and dev text,
and extends ai-companion, new-versions, benchmarks and intel-mac. New
areas: platforms, ai, community.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`Game::to_network` wrote damage alone into SetCreativeModeSlot, and
`item_from_network` read back damage and enchantments, so anything else a
creative stack carried was gone once the server echoed the inventory: a
named sword lost its name, trimmed armour its trim, an enchanted book its
stored enchantments and its repair cost.
`item_sync` is the pair for the components Horizon models: `encode` writes
them with the synced registries' ids (an unknown name is left out rather
than sent as id 0, which is a different entry) and `apply` reads them back
into the same `StackData` fields `item_data::decode_components` fills.
Potions are not carried yet: `minecraft:potion_contents` has no id or codec
in horizon-protocol, so a creative potion still arrives as an uncraftable
one (#1832). Nor are containers, charged projectiles, written books and a
map id, which the packet models but this patch does not write.
Refs #1832
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
A cow summoned with max_health and movement_speed bases, and a zombie
with an attack_damage base and a saved Health, against vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- createExperience floors count * experience and adds one with the chance of
the fraction (a level random nextFloat); flooring alone paid nothing for a
single iron ore (0.7).
- The payout pops ExperienceOrb.award's orbs at the player, recipe by recipe,
instead of adding points directly.
- A server's player (the default single-player path, LAN and dedicated)
gets it too: lan_machine_click pays when the guest takes from the output.
- AbstractFurnaceBlockEntity.preRemoveSideEffects pays at the block's centre
on any removal (explosions, /setblock, creative breaking), after the
contents spill, so it moves from break_block to the removal side effects.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client's menu and the server's guest menus now share
menu::fits_inside_container_items, and a resource-backed test checks
that a guest's shulker box menu refuses shulker boxes by pickup,
shift-click and number key while other containers take them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- ServerPlayer.die sends CommonComponents.EMPTY to the death screen when
showDeathMessages is off, as the local player's death_message already
does; the combat record is only read when the rule is on.
- A guest's CombatTracker leaves guest_combat with them, so a long-running
server doesn't keep one per connection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Leave ServerPlayer.die's message as it was: with showDeathMessages off
vanilla sends CommonComponents.EMPTY to the death screen, and #1858
reworks the death message from the combat record.
- The effects panel and recipe book tooltips use Names::display_get, so a
key missing from the language still falls back instead of showing the key.
- net::load_language reuses the language lang already loaded (packs
included) instead of reading it from disk again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The three harnesses add commands to the same LocatorBarClient lines;
this branch now builds on #1847 (itself on #1843), keeps all three
commands, and test_cursor_death_parity.py runs through
locator.one_player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both add the same respawn command to LocatorBarClient; this branch now
builds on #1843's, and test_pearl_death_parity.py runs through
locator.one_player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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.
test_hardcore_respawn_parity.py copied test_locator_bar_parity.py's
build and run code; both now share locator.sides, and one-player
scripts run through locator.one_player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Modrinth App keeps a Fabric or Quilt profile's vanilla client jar as
`versions/<v>-<loader>/<v>-<loader>.jar`; a player who only played such a
profile has no `versions/<v>/`, and Prepare said Minecraft was not found.
Those jars are now candidates too, checked against the pinned sha1 like the rest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both PRs add a stillValid check to the same tick functions; this branch
now builds on #1827's lan::guest::within_block_range for the guest's and
the in-frame table checks, and the in-frame check sits beside the
container one in storage.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reach test was written out four times (stations, lecterns, the
in-frame container check and the guest's); they now share
lan::guest::within_block_range.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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.
🤖 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.
🤖 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.
When an update's preparation reused the game files an earlier version imported,
`preparer` handed the chosen folder as it was to the sound copy and the panorama,
fonts, packs and languages step. A Prism Launcher instance or Modrinth App profile
holds no asset store, so the game was silent with a grey title screen, and
Prepare again did the same. It now resolves the folder (linking another
launcher's files into the official layout) either way; with reused imports a
missing one is a log line, not an error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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.
file:///C:/x left /C:/x after the scheme, which Windows can't open; the pack tests now
write real file:/// URLs (the index only accepts those), with forward slashes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
build.mjs reads website/data/roadmap.json since the hub (#1819), but the Dockerfile never
copied data/, so every site deploy since then failed and playhorizon.dev kept the old build.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New WIT interface `ui` (text, item and list widgets; open-screen,
close-screen, set-settings) plus a `ui-events` export (select, closed,
settings-opened), in a second world `horizon-ui-mod` so mods built before
it keep loading. The game lays out and draws the widgets with its own
menus' code: a screen the mod owns (Esc/Done to leave) and a Settings
button on the mod's Options > Mods row that opens its page.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A Windows temp path's backslashes went into the test index's JSON as escapes, so the
index failed to parse and the Windows launcher release stopped at its tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
- `new --template content`: a content mod with one block (Glowing Amethyst,
copying vanilla's amethyst block) and its block state, model, item model,
name, loot, recipe and pickaxe tag; `--template code` is the default.
- `build` packs the mod's `assets/` and `data/` (read and checked as the
game reads them) beside the component; a folder without `Cargo.toml` is a
content mod and needs no compiler.
- `check` takes a `.hmod` with no `mod.wasm`: "no code: content only,
nothing to run" instead of the trial run, which refused it, so the
registry can publish content mods.
- The guide (content mods, make a mod) and docs/mods.md, docs/api/mods.md
say so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's death only had `death_cause`, which `/kill` alone sets, so a
server-thread, LAN or dedicated death fell back to "X died" or a generic
cause. Give each guest its own CombatTracker, fed by the motion events
the server already records, and take the message from
CombatTracker.getDeathMessage with the dying player's own name. The
wither's skull names the wither that launched it (projectile owner).
The death screen now resolves the server's component in the client's
current language instead of the session's en_us table, so it matches the
chat line.
Co-Authored-By: OpenCode DeepSeek V4.1 Flash <[email protected]>
RecipesUsed was written as an empty compound and never read, so a furnace
paid nothing when its output was taken and nothing when it was broken.
- Furnace keeps the per-recipe counter, saves and loads it, and floors each
recipe on its own as AbstractFurnaceBlockEntity.createExperience does.
- FurnaceResultSlot.checkTakeAchievements pays on a take: the counter is
read and cleared when the output slot loses an item.
- AbstractFurnaceBlockEntity.playerDestroy pays the same counter as orbs at
the block when it is broken, whatever the tool.
Fixes#1732
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Six pages under a "Make a mod" group of the docs: set up and see a mod in
the game (the launcher's Game arguments or cargo horizon-mod run), the
manifest and every capability, the hooks and calls with a worked example
(Playtime, built and checked), content mods, testing and check, and
publishing (minisign signing, the submit form, review, updates). /mods,
the submit page, the docs index, Mods and data packs and the Mod API link
to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
Where to start is playhorizon.dev/hub: the roadmap, the work board, good first
tasks and the forms; roadmap PRs carry their roadmap:<id> label, ideas go to the
suggestion queue and issues are for bugs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each item still to come gets a "Want it" button with its count; the Contribute
tab gets a bug report form (the #horizon-bugs template, which the telemetry
server opens as a GitHub issue and links back) and a suggestion form (to the
private queue Kira relays), and a Contributors list that shows the Discord name
of those who agreed on Kira (/contributors.json). The forms and buttons appear
once the hub's server answers; until then the page points to GitHub and Discord.
roadmap/README.md documents the intake API that Kira shares.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The repository's docs/api/ (the portal, its surface pages and the generated mod and
server references) is built into the docs section: /docs/api is its README and
/docs/api/<page> each page, in two nav groups, "Build on Horizon" and "API reference".
The pages are read from the checkout or from main on GitHub, like the README, so the
site never holds a copy. Links between portal pages become the site's (and the build's
link check covers them); other relative links go to the file on GitHub.
The renderer gains tables: the "Ways in" table, the references and the other tables
on the site (apis, performance) showed as raw pipes. A link whose label is a code span
no longer throws.
Caddy and the dev server route /docs/api/<page> and redirect /docs/api/ to /docs/api;
Railway rebuilds when docs/api/ changes. /docs/mods links the Mod API, its reference
and its ways in; the summary page is now "APIs at a glance".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
POST /hub/report opens a GitHub issue at once (labels bug and from-website, the
every template field required, people's text made inert (no mentions, issue
references, HTML or closing keywords) and the same report twice answered with the
first issue. If GitHub fails it answers 202 and the upkeep tries again for a week.
Off (503) without HORIZON_TELEMETRY_GITHUB_TOKEN.
POST /hub/suggest queues a suggestion; GET /hub/suggestions?after=<id> hands the
queue to Kira with the HORIZON_TELEMETRY_HUB_TOKEN bearer (404 when unset, 401 for
a wrong token). Nothing is published.
Per-address limits (20 an hour, bursts of 2) on keyed hashes of the address, daily
caps from everyone (30 reports, 200 suggestions), a honeypot field, entries kept
90 days.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A visitor voting down the roadmap, or a school behind one address, ran out of
the 12-request burst. Votes are cheap; the item cap and the voter id still bound them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A stack held on the inventory cursor at death stayed there through the
respawn, even with keep_inventory off. In vanilla the dead player is
removed 20 ticks after dying (or when it respawns sooner), and
Player.remove's inventoryMenu.removed drops what the cursor held where
the player died. Do the same for server players and the in-frame
player, and add a harness that dies with a cursor stack on a dedicated
server and compares with vanilla.
The index already carries each project's owners (the GitHub accounts the
registry checked at submission), but neither the website nor the launcher
showed them. The project page now says "Published by @login" with the
avatar and a link to the profile; the launcher adds an @login link per
owner. Only strings that are valid GitHub logins become links.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShulkerBoxSlot.mayPlace refuses items that cannot fit inside container items, which BlockItem says of every shulker box. The server's menus for guests treated a shulker box's slots like a chest's, so with the server thread a shulker box could be clicked, shift-clicked or swapped into another. Apply the slot rule when the open menu is a shulker box.
With ender_pearls_vanish_on_death on, a pearl thrown just before death
kept flying and pulled the respawned player back to it. Vanilla's
ThrownEnderpearl.tick discards the pearl instead of ticking it while
its owner is dead. Add a harness that throws a pearl, dies and respawns
on a dedicated server and compares the teleports with vanilla's.
One entry per merged pull request (with its roadmap item), game or launcher
release, shipped roadmap item (post and announce) and weekly summary, newest
first, ids stable forever: the feed kira's src/journal.js posts to #updates and
X. The journal's item links are hub#<id>; the board's and the journal's ids are
reserved too, and the page-wide unique-id test covers every tab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the server thread, the host's copy of a player never set isInPowderSnow from the blocks along its moves and never ran LivingEntity.aiStep's freezing, so a frozen player took no freezing damage and the client's own damage was overwritten. Record powder snow's FREEZE on the server path and run the freezing countdown and its damage every 40 ticks there. The guest's freeze-immune armor now comes from its worn armor, as for the local player.
/ride now looks up a mob's vehicle as well as the player's, so mobs
can mount mobs, get the already-riding and loop errors, and dismount
to the vehicle's dismount spot. execute on vehicle uses the same
lookup. The command_exec corpus gets a mob riding group.
A hardcore death offered another survival life. Vanilla's death screen
says Spectate in hardcore, and the server makes the respawned player a
spectator (ServerGamePacketListenerImpl.handleClientCommand). Do the
same on the server and in the in-frame world, and add a harness that
compares a hardcore respawn on a dedicated server with vanilla's.
Kanban: every open issue and pull request and what closed in the last seven days, placed in To do,
In progress, In review and Done by the rules of Kira's work board, each card with its roadmap item
(its roadmap:<id> label or the item linking it) and a filter by item. Journal: every release with its
changes grouped by roadmap item, the merges no release has yet, and a weekly summary from
roadmap/journal/<year>-<week>.md. The same journal is /hub/journal.json, for Kira's #updates and posts
elsewhere: each entry names the roadmap items it moved and shipped, with their 240-character text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Kira (kira #10-#14) fetches https://playhorizon.dev/data/roadmap.json and links
items as <hub>#<id>: the JSON moves there (readable from any origin) with the
announce flag, and each item's anchor is its id. Ids the page uses itself (tabs,
sections, headings) are refused by the check; a test keeps every id of the page
unique, which caught the overview's headings sharing ids with the roadmap's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pass the crystal damage cause through destruction so explosion damage removes crystals without another blast, including during dragon respawn. Keep fight callbacks and direct-hit blasts.
Co-Authored-By: OpenAI Codex <[email protected]>
playhorizon.dev/hub is the public entry point to what is happening on Horizon. Overview: where
Horizon is, the 3 to 5 things in focus, what is next, what shipped last, how to help. Roadmap: Now /
Next / Later / Help wanted / Shipped, filtered by area, every item with its issues and pull requests
in their GitHub state (baked at build, refreshed in the browser). Contribute: good first tasks,
claiming, connecting an agent to Kira, the rules. A Simple / Technical switch swaps every item's
plain and dev text and is remembered. /roadmap redirects to the Roadmap tab.
tools/roadmap.py build now also writes website/data/roadmap.json, which the site reads and serves at
/hub/roadmap.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Kira's X poster (kira #12) posts shipped items flagged announce in journal.json.
Set on the five whose release is news: modpack world generation, the mod
registry, Discord Rich Presence, Microsoft sign-in and CurseForge. Flags must be
booleans.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
A thumbs-up on a roadmap item at playhorizon.dev/hub: one per browser (a random
id the page keeps), taken back with "up": false, counts cached a minute. Item ids
are checked for shape and capped at 1000 distinct items. Rate limits key their
buckets by a keyed hash of the address (key drawn at start, in memory); no address
is stored. CORS preflight for the website's JSON posts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
LivingEntity.readAdditionalSaveData applies the attributes list for any
living entity, then sets the health (the max health when none is saved).
Only horses read max_health and movement_speed, so a summoned cow kept its
type defaults. Read max_health, movement_speed and attack_damage for all
mobs before the health; horses still read jump_strength.
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
Chat, /say, /me and /tell now name the sender the way
Player.getDisplayName does: the team prefix, the name and the suffix
in the team colour. On a server the hub gets the scoreboard's teams
each tick; in-frame the existing player_display_name is used.
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
A container click from a server-thread or LAN player wrote the slots but
never called BlockEntity.setChanged, so comparators kept the old output.
Notify the block entity when the click changed the container's slots.
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
CraftingMenu.stillValid needs the block to still be a crafting table and the
player within reach of it. The table's position is now kept with the open
grid, so the menu closes in the tick (giving its grid back) and a stale
click no longer crafts, in single player and for LAN guests.
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
Container.stillValidBlockEntity also checks the player's block interaction
range plus a 4-block buffer. Guest menus now close in the tick once out of
reach and ignore clicks while invalid, and the in-frame single player menu
closes the same way.
The SDK's tool, in horizon-shader-api: `horizon-shader new <dir> --template
post|sky|material` makes a pack that passes the check, `check <pack>...
[--json]` runs the check the game, the launcher and the registry run and
prints each stage's cost and the pack's SHA-256 or every problem at its
file:line:column, and `pack <dir>` checks then writes the reproducible
.hshader. Golden Hour, the example pack, warms the sky around a low sun and
adds a soft glow and a colour grade, with five options. docs/shaders.md is the
authors' guide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_shader_api::check builds each stage as the renderer does and judges
it the same way wherever it runs (the game, the launcher, the registry's CI,
the coming horizon-shader tool): naga parses and validates it; the stage
function must have its signature; the pack's files declare no bindings, no
entry points and no horizon_ names; each override is an option of its type or
has a default, and each option is used; a stage stays within 20 000
expressions and 64 texture reads; and every entry point translates to SPIR-V,
Metal and HLSL with the options at their defaults, minimums and maximums.
Problems carry the pack's own file, line and column. Pack::sha256 gives the
reproducible package's hash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WitherRoseBlock.entityInside adds getBeeInteractionEffect (Wither, 40
ticks) to a living entity inside it on a server level that isn't
peaceful, unless it is invulnerable to wither damage. The player's
inside-block dispatch had no wither rose case, so neither singleplayer
mode applied it. The sweep now asks for it, the game adds it to its own
player, and the server-thread path adds it to a server's player.
Fixes#1319.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 reconnects after ClientboundTransferPacket with a TransferState,
so Connection.initiateServerboundPlayConnection sends ClientIntent.TRANSFER (3):
a target with accepts-transfers=false refuses the player, and the target can
ask for the carried cookies during login
(ClientHandshakePacketListenerImpl.handleRequestCookie). Both now match.
A transfer from single player closes the world instead of connecting
elsewhere (handleTransfer: "Cannot transfer to server from singleplayer").
Adds a unit test for the play-state store/request round trip and the
Transfer event.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three stages, each a WGSL function with a fixed signature that Horizon calls
inside its own pass: sky (fn sky(input: SkyInput) -> vec4<f32>), material
(fn material(input: MaterialInput) -> Surface) and post (fn post(input:
PostInput) -> vec4<f32>). A stage's WGSL is a generated prelude (the frame
uniform, the stage's structs, its resources in bind group 3, the post
helpers), the pack's shared files, its own file, then the entry points; each
part is located so errors point at the pack's file and line.
The frame is the Rust struct the renderer will upload; its WGSL, its offsets
and docs/api/shaders-reference.md are generated from it, and tests check that
the preludes compile, that naga's layout of the WGSL is the Rust layout, that
the sample pack's stages compile, and that the reference is up to date.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new crate, horizon-shader-api, reads and checks Horizon's own WGSL shader
packs (docs/api/shaders.md), so the game, the launcher and the registry judge
a pack the same way. This first part is the format: the manifest (id, name,
version, the api line, shared files, the sky, material and post stages,
options typed for WGSL override constants, a GPU time budget) and the package
(a folder or a .hshader zip, built reproducibly so its SHA-256 depends only on
its files). The stages' WGSL is checked in the next parts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/mods.md gets "Publish your mod" (the form, the review, updates) and
its Distribution section points at the registry instead of the plan for
it. docs/api/mod-index.md says how horizon-mods runs: its checks, merging,
Pages, the weekly cleanup, and that removing an entry needs an admin merge
(takedowns go in revoked.toml). The service's README says where it runs on
Railway.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The title bar and the theme both resolved HORIZON_LAUNCHER_APPEARANCE and the
Settings choice; titlebar::apply_appearance now uses the theme's helper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One source of truth for what is being done, what comes next and where to help: roadmap/roadmap.toml,
each item said twice (plain words for players, the technical picture for contributors), Now / Next /
Later / Help wanted / Shipped, no dates. tools/roadmap.py builds ROADMAP.md from it, checks its rules
offline (CI, through tests/test_roadmap.py) and against GitHub (links resolve, no stale Now item),
ships items whose pull requests all went out in a release (sync), and keeps a roadmap:<id> label per
item (labels). README links it; roadmap/README.md is the maintenance guide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ZombieHorse.finalizeSpawn gets its rider from EntityType.create, which
returns null for a notInPeaceful type (the zombie) in peaceful, so vanilla
spawns the zombie horse alone there. spawn_jockeys created the rider
regardless. mobs/finalize.rs already skips it through level.create.
Fixes#1761.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The form opens pull requests with the registry's token, which is Ludovic's
account, so the body now starts "🤖 Submitted through playhorizon.dev by
@login" and says neither Ludovic nor a reviewer wrote it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BaseFireBlock.inPortalDimension: a frame lit in the End stays a fire.
The world code's fire placement already checked it; the flint-and-steel
path (Game::try_light_portal) didn't, so a portal could be lit and used
in the End in every mode.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundCookieRequestPacket in play state decoded as Unknown and was
dropped; StoreCookie and Transfer likewise, and the configuration-phase
CookieRequest was always answered empty. Proxies (BungeeCord, Velocity)
store and read cookies around a server switch and send Transfer to move
a player to another address — both ended in a kick.
Play-state cookie_request/store_cookie/transfer now have packet types,
cookie requests answer from the stored cookies in both configuration and
play (ClientCommonPacketListenerImpl.handleRequestCookie), stored cookies
land in the session's map (handleStoreCookie), and a transfer reconnects
the game to the target address carrying them (handleTransfer →
ConnectScreen.startConnecting with TransferState).
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- item and block names in tooltips, held labels, command feedback and the
creative tabs read the selected language; the pinned English map stays
for logic and search
- Game::translate resolves through lang::get_or, so station titles follow
the language screen
- container titles and Inventory labels for chests, hoppers, furnaces,
smokers, blast furnaces, crafters, shulkers and mounts are translated
- server-resolved components (menu titles, death messages, command
suggestion tooltips) load the selected language over en_us, and follow
a language change while a world is open
- death messages always reach the death screen translated; the chat
broadcast still obeys showDeathMessages
- creative search matches the localized display name
- sign editors use sign.edit and hanging_sign.edit
Co-Authored-By: OpenCode DeepSeek V4.1 Flash <[email protected]>
Each chunk's compiled rule is specialised once for every band of y that
its vertical gradients bound, and for blocks below and at or above the
preliminary surface when the rule asks for it. Conditions those facts
decide are folded away, so a deep stone block evaluates only the rules
that can apply there (ore veins, the underground rules) instead of
walking the whole surface tree. Conditions only read the position and
the world, so the blocks are the same.
Surface stage, one thread, seed 1234: 2.45 -> 2.08 ms per chunk.
postprocess_chunks ran before tick_blocks registered the chunks that
arrived since the last tick, so the fluid and block ticks their marks
schedule went to chunks the scheduler did not know and were dropped.
Vanilla registers a chunk's tick containers before postProcessGeneration.
attach_save_data took a chunk's ticks only from the tick scheduler, which
registers new chunks once a tick. A chunk loaded and unloaded again in
between was saved with its block and fluid ticks cleared, though its own
saved list still held them.
Mob.DATA_MOB_FLAGS_ID now carries the aggressive bit (with its default
0, so it goes back off) and every client mob reads no AI, left-handed
and aggressive from it: a vindicator draws its axe, skeletons their bow,
piglins and illagers their attack poses on the server thread, LAN guests
and dedicated servers. Mobs' swings (LivingEntity.swing) go out as
ClientboundSwingAnimationPacket.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
authlib 10.0.77 MinecraftServicesSessionService.unpackTextures returns
MinecraftProfileTextures.EMPTY when any texture has no URL or a URL outside
the allowed domains, so skins::unpack goes back to main's behaviour.
remote_player_names: r.players and r.tab_players are filled from the same
ADD_PLAYER entries and emptied by the same PlayerInfoRemove, so the
tab-list fallback could never apply.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server's client (single player on the server thread, LAN guests), a
shovel making a path, an axe stripping, a hoe tilling or honeycomb waxing
did not end the right click: the client went on to the other hand, so an
off-hand torch was predicted as placed and its hand swung instead of the
tool's (#1289).
Vanilla's client runs `Item.useOn` itself: its SUCCESS swings that hand
and `Minecraft.startUseItem` returns before the off hand. The client now
does the same when its prediction of the tool's use succeeds.
Fixes#1289
Co-Authored-By: Claude Opus 5.5 <[email protected]>
sockets() reads the environment through a function, so a unit test can
check the paths a real Discord listens on for each platform ($TMPDIR on
macOS, $XDG_RUNTIME_DIR and its Flatpak and Snap subfolders on Linux,
named pipes on Windows). Offscreen e2e runs can't cover this: an automated
run only talks to a fake Discord named by HORIZON_DISCORD_IPC_DIR.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server's AbstractContainerMenu.removed gives the grid and the
carried stack back to the inventory and syncs those slots, but the
client kept its copies: a crafting table closed with items inside left
ghost stacks in the inventory's 2x2 grid that could not be picked up.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A server's client (single player on the server thread, LAN guests, a
dedicated server's players) saw every potion as an "Uncraftable Potion"
in the water colour: the server never wrote `minecraft:potion_contents`
and the client read past it. A potion taken from the creative menu also
reached the server without its potion, so it splashed as water.
PotionContents now goes as vanilla's stream codec writes it: the potion
by its `minecraft:potion` id and the custom colour, with the custom
effects and name kept as their bytes. The server writes it for every
stack it sends, the client reads it into the item's components, and a
creative pick and the server's read of it keep the potion too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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>
skins::unpack discarded a profile's whole textures payload when any one
entry lacked "url" or pointed outside the texture services' domains, so
a player with e.g. an unusual cape lost their skin too; it now drops only
the offending texture. The 32 skin atlas slots evicted past players back
to defaults on busy servers — now 128. A dimension reset (join game or
respawn onto another world) now also clears the entity-id→profile map so
a re-used entity id can't pin the wrong skin or name to a player, and the
nametag join falls back to the player-list entry's name.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- FlyTowardsPositionParticle puts vault_connection on the translucent
layer: its LifetimeAlpha(0, 0.6, 0.25, 1) is not opaque.
- The shadow draw started at world_quads - shadow_quads; with the new
bucket after it, that drew translucent particles with the shadow
pipeline and dropped as many shadows.
- One atlas lookup per named-sprite particle and frame: the engine keeps
each sprite's layer beside its rect instead of hashing the name twice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Quad particles all went into the entity batch, drawn before translucent
terrain with depth writes on: a stained-glass crumb wrote its depth and
the sorted translucent pass then culled the glass behind it, so breaking
glass in front of glass cut holes through the wall.
Vanilla 26.3 submits quad particles to two phases: opaque layers draw
with the solids and translucent layers draw in an "afterTerrain" phase
right after the translucent section pass (the translucent particle
pipeline still writes depth, so ordering is what matters). Mirror that:
HudAtlas records which sprites have a 0 < alpha < 255 texel
(`SpriteContents.transparency().hasTranslucent`), particles classify
their layer the way `SingleQuadParticle.getLayer` does — the type's
hardcoded layer, or `Layer.bySprite` for terrain, item and block marker
sprites — and the entity batch routes those quads to a sixth bucket the
renderer draws immediately after translucent terrain.
Fixes#1679.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
#1665 moved the save path into finish_open_world and #1666's merge kept the
old line, so clippy -D warnings fails on main (unused variable) since 7d0ae5658.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clear remote passenger mount and pose flags when a vehicle disappears, matching Entity.setRemoved. Add a two-passenger boat destruction check for both single-player paths and LAN observers.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Let PlayerCommand control the server sprint flag, matching vanilla, so releasing Ctrl cannot leave the client's speed modifier and FOV stuck. Cover the packet sequence and FOV recovery in regression tests.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Each frame the game hands the presence thread its status (menus; single
player, multiplayer or hosting a world with its party size; the
dimension; the game mode), and only a change wakes it. The switch
"Discord Rich Presence" (on by default, discordPresence in
horizon-extras.txt) sits beside Controller Settings. Dedicated servers
never start it. tools/discord_presence_e2e.py runs real games against a
fake Discord: title, world, Nether and quit on the server thread and
in-frame, a LAN host (both paths) and guest, the switch, and a Discord
started after the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Discord's "Horizon" application (1563857609738690560, public) has the
horizon logo as its large image; no game-mode images yet. The feature is
live: only HORIZON_NO_DISCORD=1, the switch and automated runs keep it
quiet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
discord_presence.rs speaks Discord's local IPC itself (discord-ipc-N
socket or named pipe; opcode + length + JSON frames; HANDSHAKE, then
SET_ACTIVITY with the pid) from a thread of its own. The game hands it a
status; the thread connects lazily, retries every 15 s, keeps within 5
updates per 20 s, refreshes every minute and clears the activity on
quit. Inert while DISCORD_APP_ID is the placeholder, with
HORIZON_NO_DISCORD=1, and for automated runs unless
HORIZON_DISCORD_IPC_DIR points at a test's fake Discord; lab.game_env
sets HORIZON_NO_DISCORD=1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Performance page now gives the launcher command (horizon-launcher play --arg=--benchmark
--arg=standard), where the report lands on each system, what it holds, and the two ways to
share it: horizon-launcher data benchmark, or the Discord.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ZipFile refuses a 0xFFFFFFFF size without its Zip64 extra field ("invalid
zip64 extra len size"), so the test builds both shapes it accepts: real
sizes, and the sentinel with the extra (the zip crate reads the real size
from it). With every accepted shape carrying a real size, the inflate
limit needs no special case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved the removal to a thread in #1715 (Msg::Removed, the removing
row, the selection), which is this branch's UI half: main's version is
kept. What stays is backend::remove's retry on a file Windows still
holds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lang::get_or, as the title screen's labels do; en_us 26.3 has
multiplayer.downloadingTerrain = "Loading terrain...".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflict in tools/lan_e2e.py's check list: main added shulker_bullets
beside crystals; both kept, crystals' entry aligned with the others.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Projectile.addAdditionalSaveData writes LeftOwner only when it is set and
HasBeenShot always (true from the first tick on); fireball_to_nbt wrote
LeftOwner:0b and no HasBeenShot. Same shape as arrow_to_nbt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.teleportSetPosition calls setYRot, setYHeadRot and setXRot, then
setOldPosAndRot: the head faces the new yaw and yRotO/xRotO match, so a
teleported mob doesn't sweep from its old pitch on the next frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The twenty unwrap_or_else(|e| e.into_inner()) calls become one lock()
helper, so the bots read as they did with the same poison tolerance.
bound_property moves from worldgen.rs (between DecodeError's impls) to
block_predicate.rs, next to PredicateBlockResolver, whose
predicate_property it implements.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FriendsButton's message is gui.friends.open, so the title screen needs
no Horizon key for it. The fallbacks are en_us's own text for the
three icons ("Language...", "Accessibility Settings..."), as the
other labels' are.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TitleScreen's Friends button is vanilla's FriendsButton, whose message
is gui.friends.open ("Friends" in en_us, translated in every language
the prepared resources carry), so Horizon needs no key of its own for
it. Bundled now sits after the trait it implements.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the removal off the UI thread, the row kept its Use and Remove
buttons, so a second Remove or Use could race the deletion. The retry
loop also reads as one match now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DedicatedPlayerList logs "Failed to save user banlist: " (and ip
banlist, operators list, white-list) when a startup save fails; the
shared closure printed "Failed to load user banlist write". The closure
now takes the verb, so loads and saves read as vanilla's lines.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
receive_decorated moved the nine protos out of the box anyway (the
array iterator holds them inline), so taking [Proto; 9] and unboxing at
the three call sites costs the same and leaves boxed_local nothing to
flag on any target. The #[expect] failed on Linux, where the lint does
not fire, which is why verify was red.
scratch is only called from the #[cfg(unix)] tests, so it carries the
same cfg as installed() next to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Include End crystals in attack-only object hit-testing using their network entity IDs. Add a real-mouse LAN regression for survival and creative, reach, block occlusion, passing item use, and explosion damage.
Co-Authored-By: OpenCode GPT-6.1 <[email protected]>
Sounds can't be observed playing without an audio device, so the control
status counts each stopped selection, as it counts the sounds heard.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TextDisplayRenderer.splitLines uses Font.split (no indent on wrapped
lines, unlike chat's wrapComponents), and TextDisplay keeps the result
until its render state changes; the frame no longer wraps and measures
every display again. A render state is rebuilt only for the ids vanilla
lists (RENDER_STATE_IDS, TEXT_RENDER_STATE_IDS) and starts from the
progress the last frame drew (Display.lastProgress).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every block the mesher draws asked the model catalog for its models: a
BTreeMap lookup of the state, a Vec of the chosen variants, then for each
variant a clone of its model name and a string-keyed BTreeMap lookup of the
baked model, collected into a second Vec. On generated terrain that was a
fifth of the mesher's time.
The catalog now resolves each variant to its baked model's index once, at
load (CompiledChoices maps its variants), keeps states in a Vec by id, and
the mesher reuses one list of models for every block of a section. The
random draws are the same calls in the same order (CompiledChoices::for_each
is the old collect), so the same models are chosen.
examples/chunk_load_bench.rs generates and lights real terrain with the
vanilla generator and meshes it on one thread, with a checksum of the meshes:
identical meshes, 45.8 -> 35.4 us per section on the Mac mini.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
StopSoundCommand sends ClientboundStopSoundPacket to every target. The
command stopped the server's own sounds instead, so on the server thread
(single player's default) and for LAN guests nothing stopped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fs::canonicalize returns verbatim paths on Windows, and the chosen home
goes into JAVA_HOME and the PATH of external preparers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server's player tick dropped FallEvent::Destroy, so an egg a guest
broke by landing left its neighbours unupdated. Both paths now share the
same helper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each step waited a fixed 2.5 s. When the machine is loaded the vanilla server stalls for longer than that (CI on
#1571 and #1585: after Bob's trip to the Nether, vanilla's `tp Bob 4.5 -60 0.5` from the console ran a step late),
so its packets were filed under the next step and the transcripts differed although both servers sent the same.
A step now ends once the server has done its work:
- the console's own `time query gametime` comes back, so the step's console commands have run (the console runs
its queue in order);
- LocatorBarClient's new `sync NAME` runs `time query gametime` as that player and waits for the reply (not
recorded), so that player's commands have run and every packet sent before the reply is recorded;
- the server's game time has advanced STEP_TICKS (40) more ticks, then each player sends its position reminder
and syncs once more.
game_time() moves out of settle() in test_dedicated_server_parity.py so both use it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/random reset * in the ticking group counted 12 sequences in vanilla against Horizon's 11 on one CI run (#1585).
Locally 1 oracle run in 24 did the same, and its extra sequence was minecraft:entities/bat. Before the setup's
gamerules (spawn_mobs and mob_drops off), the oracle ticks until the entities around the player have loaded, a
number of ticks that depends on the machine; mobs spawn meanwhile, and a bat that died then rolled its loot table.
Once mob_drops is off no mob death rolls one (LivingEntity.shouldDropLoot), so the setup now ends with an
unrecorded /random reset *: every run starts the script with no sequence, and the later count holds only what the
corpus itself rolled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move, no behaviour change. The conversion of YUNG's API and its mods,
Lithostitched and ChoiceTheorem's Overhauled Village was mixed with the general
conversion code in worldgen.rs and processors.rs. It now lives in
crates/horizon-port-pack/src/legacy_structure_mods/{yung,lithostitched,ctov}.rs,
and the general code reaches it through a few entry points, so a clean-room
rewrite can replace the module without touching the rest.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Read Git's pre-push ref updates and inspect the full reachable history of each non-deleted local object ID. Ignore unrelated refs while rejecting private artifacts in deleted files, merge-only paths, and stored history masked by local replacements.
Add regression coverage for branches, tags, raw revisions, multiple updates, deletion-only pushes, and artifact detection.
Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
Clean-room, from spec-yungsapi.md §2.1 and §3.1 (no mod source read).
A `yungsapi:yung_jigsaw` structure lays out with the jigsaw assembler and
YUNG's draws: the start pool's file entries shuffled, then the rotation,
then a weighted pick in the shuffled order; each jigsaw's target pool is
drawn by weight from its shuffled entries without replacement, an element
whose `name` already has `max_count` pieces skipped at no other draw.
`yungsapi:yung_single_element` places what a single element places; its
pool entry carries the name and limit.
tools/test_structure_layout_parity.py --profile betterdungeons reads the
mod's own data from its jar into an overlay of the 26.3 resources and
compares the mod world's pieces: 82 of 84 Better Dungeons starts match
piece for piece (60 small, 11 skeleton, 11 zombie dungeons, up to 790
pieces); the other 2 sit in 26.3's sulfur_caves, which 1.21.1 lacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reuse stop_riding before applying a player teleport destination so the riding tick cannot return the player to the vehicle. Add vanilla command parity and both-mode LAN regressions for mounted teleports.
Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
Preserve the Hero effect captured by the guest's merchant session and cover discounted prices and payments for both single-player modes.
Fixes#1365
Co-Authored-By: OpenCode (gpt-6.1-sol) <[email protected]>
Pair PATH tools from a compatible JDK and always forward its home to external preparers so Java 25 exporters run with a compatible runtime.
Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
Notify connected players of the display IDs forgotten by /recipe take.
Discard queued additions for recipes the player no longer knows.
Add LAN regression coverage for both single-player paths.
Co-Authored-By: OpenCode agentrouter/gpt-6.1-sol <[email protected]>
Decode source/event selection flags, including 26.3 UI source 10, and route them to Audio.stop_sounds. Compare all 36 native Java codec selections and round-trip their bytes; reject invalid sources and truncated packets.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Move the spectator-travel check before the existing villagers check so it merges cleanly with the newly added falling check on main.
Co-Authored-By: OpenCode agentrouter/gpt-6.1-sol <[email protected]>
Assign mob-fired projectiles IDs when they are added and recognize shulker bullet spawns on remote clients. Extend the live LAN checks for both hosting modes.
Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
Apply metadata, lifetime and position/transform interpolation; render wrapped/aligned text, background, opacity, shadows, brightness and see-through flags. Upload dynamic font glyphs and expose actual emitted vertices for end-to-end checks. Verify 108 matrices with the native client renderer and exercise a real Java-server/Rust-client fixture.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
The clean-room structure oracle saved, for 31 starts of vanilla structures
(villages, outposts, trial chambers, shipwrecks, ruined portals, desert
pyramids over seeds 1234, 2 and 42), the no-mod world's pieces. A new
structure_pieces probe lists the starts a chunk owns, piece by piece, and
tools/test_structure_layout_parity.py compares them with those saves.
22 of the 31 match piece for piece (every village and outpost that 26.3
still places there, and the horizontal boxes of the code-built pieces).
The other 9 differ between 1.21.1 and 26.3 themselves: 26.3 vanilla's own
pieces there (VanillaChunkGenerationOracle scan) equal Horizon's for all 18
starts that differ from 1.21.1. No vanilla jigsaw fix is needed.
Templates now keep the id they were loaded under, so a piece names its
element.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Forward landing block-write events to notify_written so unsupported crops break, drop their items, and synchronize to clients. Add LAN regression coverage for the host and guest in both hosting paths.
Co-Authored-By: OpenCode GPT-6.1-sol <[email protected]>
Add a native Java oracle for the actual Display/TextDisplay accessors and encoded metadata, with 513 combinations of flags, opacity, billboard mode and transforms. Preserve styled text and partial updates.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Clicking Remove deleted the version's thousands of files on the UI thread, so
the whole launcher froze for seconds (more on Windows). The removal now runs
on a thread and ends with Msg::Removed. The version leaves the installed list
at once (nothing can launch, select or prepare it) and its row shows
"Removing…" with a moving bar; installing it again is refused until it is gone.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Restore the current game mode's spectator flag when remote_teleport creates a replacement player body. Add an end-to-end wall traversal regression for Nether travel and return, covering the integrated-server host, LAN guest and in-frame host.
Fixes#1299
Co-Authored-By: OpenCode agentrouter/gpt-6.1-sol <[email protected]>
predicate_property was the same fifteen lines in block_feature and
state_provider; it is worldgen::bound_property now. The mojang link helpers are
unix-only in use but worth testing everywhere, so they are allowed to look dead
elsewhere, which clears the launcher's -D warnings red on Windows. The bots
harness recovers from a poisoned lock instead of panicking.
The moderation lists on a dedicated server, command storage, boss bars, the
world clocks, the difficulty and the emergency save before a crash all used
let _ = and lost the error. write_dat also swallowed the rename that moves the
current file aside, which made the following rename fail on the existing path
with nothing reporting it.
link cannot symlink a directory on Windows, so mirror has to recurse into one
itself, but it only did so when a pack had filters. An untouched directory fell
through to the link branch and vanished from the overlay, which took
textures/font and the panorama with it and made a pack that overrides a font
crash the client before its window.
A resource pack written while it streams (Nexo's server behind
resourcepack.gommehd.io, gommehd.net's required pack) leaves every local
header a stub, every uncompressed size at 0xFFFFFFFF and the END record's
entry counts and disk number at 0 and 0xFFFF. Java tolerates all three;
the zip crate's ZipArchive refuses the END record as multi-disk and empty,
so a required server pack failed its reload and the join never got through
(each retry also rebuilt the renderer, once a second).
Count the real central-directory entries and normalize the END record's
disk and count fields when they disagree with it, and bound the inflate
output when an entry's size is the 0xFFFFFFFF sentinel.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
scoreboard::canonical_component already re-encodes a component as vanilla
does, list form included, so decorate_name uses it instead of its own copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A component list is its first element with the rest as siblings, so
decorateName merges the frame colour under that element's style. The
parsed list moved a styled first element down a level, which gave the
description the frame colour instead of the title's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Advancement.decorateName merges the frame colour under the title's own
style and appends the description after its siblings. Built from the
parsed Text, a styled root with siblings had its style moved down a level,
so the description showed in the frame colour instead of the title's
(a gold, bold title gave a green description). The hover is now built on
the title's JSON, and the local spans are decoded from that JSON, so an
in-frame player sees the description coloured as a client does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server sends an item's and a falling block's position only every 20 ticks.
As a client (multiplayer, a LAN guest, single player on its server thread)
Horizon moved them only when a position arrived, three steps towards it, so a
dropped item or falling sand hung in the air and then jumped down once a
second (#1568).
Vanilla runs ItemEntity.tick and FallingBlockEntity.tick on the client too
(gravity, collisions, drag), takes their motion from AddEntity
(recreateFromPacket) and SetEntityMotion (Entity.lerpMotion), and snaps them to
each position packet: neither overrides Entity's NO_OP InterpolationHandler.
The client now does the same, with the same ticks single player in-frame uses;
only the server removes them or places the block.
lan_e2e --check falling: the guest samples both every 2 ticks over 16 ticks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A loopback test listens on both [::1] and 127.0.0.1 and checks the
connection lands on IPv4 although IPv6 is listed first; it fails without
the sort. Another checks the fallback to IPv6 when IPv4 refuses. When every
address fails, the error is now the first address's (the one vanilla tries)
rather than an IPv6 fallback's, which is often only "no route to host".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The effect capped downward gravity but left the fall distance to accumulate, so the landing tick still dealt fall damage. Clear it at the start of check_fall_damage, where both local travel and the server thread's replay of a guest's move pass.
Co-Authored-By: opencode (deepseek-v4.1-flash-free) <[email protected]>
CustomBossEvents.onPlayerConnect adds a joining player to its bars before any
entity, dragon fight or raid tells it about theirs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The item-tab hotbar loop only set the tooltip, so the nine slots never
showed the white hover fill the 5x9 grid uses. Apply the same fill,
including over empty slots, before picking the tooltip item.
Fixes#1341
Co-Authored-By: opencode (deepseek-v4.1-flash-free) <[email protected]>
remove_dir_all stops at the first file another process still holds and leaves
the rest deleted, so a removal that worked reported os error 5. Retry five times
250ms apart and treat a missing folder as done. The window froze because the
deletion ran from the update loop; it now runs on a thread and reports back
with Msg::Removed, as install already did.
The host wrote no trim component, so single player on its server thread,
LAN guests and dedicated-server players saw armour without its trim.
network_stack now takes the connection's synced-registry lookup for any
registry and writes Component::Trim when both names are in it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new home-leak test rejects scripts that set HORIZON_NO_AUDIO by hand; this one, added on main
since, did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server sends its packs in the configuration phase, while the joining
screen covers the frame; vanilla's ToastManager draws over every screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host diffed each viewer's boss events into a Change enum, then turned the
changes into packets for guests and, through a second match, into setter calls
on the window's own overlay; the client turned packets into the same setters a
third way. The diff now yields ClientboundBossEventPacket values, guests get
them as packets and the local overlay applies them with BossOverlay::apply,
the same function a server's packets reach. Gone: Change, Snap, Sent,
apply_local, boss_packet and the overlay's nine-argument add and setters.
UUIDUtil.uuidFromIntArray joins uuid_ints in lan::playerdata.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The settings screen's own options (controller, vibration, dead zones and the
rest) are not vanilla options and have no vanilla keys, so their captions
were English literals. They now read horizon.options.* from assets/horizon/lang,
which the client bundles as one more language source placed first, so a pack
or the prepared root can still override it. English and Russian ship here.
The pre-world overlay said "Joined as {name}; loading terrain..." after
Event::Joined. Vanilla's LevelLoadingScreen only ever draws the
multiplayer.downloadingTerrain text there — use that translation, no
name.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The trim decode sat inside the read-past fallback and matched the kind
there; it now has a TRIM arm, and the fallback only reads past. Long
comment lines wrapped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What the toast showed differed from vanilla 26.3 in several ways:
- the title and the failure line had no arguments: download.pack.title takes
the request number and the batch size, download.pack.failed the failures
and the batch size;
- sizes read "1.3 KiB" style; com.mojang.realmsclient.Unit.humanReadable gives
"1.3 KB" ("512 B" below 1 KiB, half-up rounding), checked against the jar;
- each update didn't restart SystemToast's 5 s display time, so a download
longer than 5 s lost its toast, and the Notification Time option was ignored;
- a finished batch removed the toast at once instead of forceHide's slide out;
- the failure title sat at y 7 in white; SystemToast draws titles yellow, and a
title without a message at y 12;
- a second batch replaced the first one's channel and lost its events.
The download thread now reports HttpUtil's listener calls (requestStart,
downloadStart with the Content-Length, downloadedBytes, requestFinished), and
each batch gets a PackNotifier (createDownloadNotifier) with its own toast. The
toast is a shared toasts::SystemToast; the system background moved to
toasts.rs with a y, so the pack toasts stack under an advancement toast.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Advancement.decorateName appends a new line and the description to the title's copy;
adv_name replaced the title's extra list, so a title built from siblings (a
plugin's {"text":"","extra":[...]}) reached clients as just the description.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The screen built them from literals, so the buttons stayed English in every
language while the world select screen next to them was translated. Each has
a vanilla key the prepared resources already carry; Friends is Horizon's own
and reads horizon.menu.friends with the English text as fallback.
Main's SRV resolver (#1578) added Connection::connect_server; both connect paths now
go through connect_addresses, which puts IPv4 first, as ServerAddressResolver.SYSTEM's
InetAddress.getByName does for the original host and the SRV target alike.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A proxy switching servers (Velocity, BungeeCord) sends the client back to
configuration. The network thread acknowledged it, but the game thread kept
ticking the old level and its client_tick_end went out right after the
acknowledgement, where the server reads it as configuration packet 13.
Velocity drops the client: "A packet did not decode successfully", shown as
"An internal error occurred in your connection".
Vanilla's handleConfigurationStart clears the level before it acknowledges,
so nothing of play follows. Session::send now drops play packets while the
connection is in configuration, checked under the writer lock that the
acknowledgement is sent with.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mesher::mesh keeps model order, which the leaf-culling test pairs with
the baked faces; the section and tile workers sort what they upload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
load_post takes the game's terrain vertex inputs and, given them, loads
the first program of gbuffers_terrain's fallback chain that translates
(gbuffers_terrain, textured_lit, textured, basic). Program loading is
shared by the post-processing and gbuffers programs.
The translator no longer rewrites a variable, parameter or member that
has the name of a sampler (`float specular` beside `uniform sampler2D
specular`), which kept Complementary's gbuffers programs from compiling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Packs sample blocks from one 2D atlas; Horizon keeps them in sprite
arrays. While a pack is loaded, every layer and its mip levels are
copied into a grid, and the animator reports the layers it rewrote so
only those are copied again. The sprite arrays gain COPY_SRC.
The gbuffers pass binds it in the next slice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
translate_gbuffers places a vertex-input glue the game writes (it knows
its own vertex formats) in front of a pack's gbuffers program, sets the
compatibility matrices from gbufferModelView/gbufferProjection and the
lightmap texture matrix, and gives declared attributes the game's values
(zero when it has none).
Nothing calls it yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GitHub's "Latest release" is the newest game release, so new players download the game's archive, which
does not start on its own (#1647). Making a launcher release the latest instead would break every
installed horizon-server, install.sh and install.ps1: they ask for releases/latest and expect the game's
archives, and a server that can't update can't fetch a fix either. So the game stays the latest and says
where to go:
- The notes open with "Want to play? Download the Horizon launcher, not the files below", linking
playhorizon.dev and the launcher releases. It is raw HTML on one line, which GitHub renders and the
launcher's notes view and one-line summary skip (pulldown-cmark HTML events), so launchers already
installed don't tell players to download themselves. The game footer that said the same at the bottom
goes. The site's Releases page leaves the block out.
- release.yml gives each archive a label ("Game files the launcher installs: Linux (...)", "Dedicated
server for Linux (...)"), which GitHub shows instead of the file name. Everything that downloads
matches the asset's name, which is unchanged.
Fixes#1647
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Give integrated and dedicated startup a synchronous loading entry point without client deferral or integrated-server selection. Keep client orchestration and background loading unchanged.
Co-Authored-By: OpenAI Codex GPT-6.1 <[email protected]>
Separate client, integrated-server and dedicated-server roles from connection state. Use roles for startup, presentation work and server dimension loading while preserving in-frame client simulation.
Co-Authored-By: OpenAI Codex (GPT-6.1) <[email protected]>
A mounted mob answered "Found no elements matching Passengers": the command
built the entity with the flat serializer, so `Passengers` was never in the
tag, even though the game had the relationship and saving a world kept it.
Persistence already writes the tree (`Game::mob_tree_nbt`, which also carries
the leash), and vanilla's command path uses the same one, so the command reads
it now.
Fixes#1519.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
New players ask on Discord whether Horizon is legit and whether it is vibecoded. One plain page answers
both, every claim checked against the repository today: agents write most of the code, what checks it
instead of a human reading every line (harnesses, verify, parity, the contribution gate, the release
smoke test), signed checksums, the macOS and Windows signing status as it is (not notarized, not
code-signed), the mod sandbox, opt-in telemetry, no Mojang files and no other project's code.
On the website (docs nav under Start here, a card in the home page's community section, the footers) and
as docs/how-horizon-is-built.md. CONTRIBUTING.md gets the public "Only original code" rule the page links.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A buffer is cleared only when a program reads it before one writes all
of it; the clears left ride in the blit pass as attachments the blit
shader leaves alone, instead of passes of their own.
- The blit writes depthtex0 only when some program reads it.
- Attachments a pass overwrites whole (the blit's colortex0 and depth,
every composite output, final's window) start cleared rather than
loaded, which spares a tiled GPU reading them back first.
- The blit's bindings are kept until the world target changes size.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
boss_event_round_trips missed its #[test] attribute, failing clippy's
dead-code lint; unknown_packets_keep_their_payload decoded boss_event,
which this PR gives a real decoder, so the garbage payload no longer
comes back Unknown — it uses player_look_at, which still has none.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The batched loop and the one-at-a-time reference were two copies of the
same 100 lines; a const generic picks the path, so the test compares the
code that ships with itself minus the batching. Drops the ignored
microkernel timer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first slice that draws: with HORIZON_SHADER_PACK=<name>, the pack in
shaderpacks/<name> (folder or zip) runs its prepare, deferred, composite
and final programs over the world, under the hand and the HUD. Gbuffers
programs come later, so the composites read Horizon's own picture.
- The world renders into its own target (as with a resolution scale); its
colour becomes colortex0 and its reverse-Z depth becomes depthtex0 in
GL's window depth (1 - d for the same planes), rows flipped to GL's
order. depthtex1 and depthtex2 read the same depth for now.
- colortex buffers in the formats the pack asked for (the nearest one
wgpu renders to), created only when a program uses them, each with two
textures: programs read main and write alt, the buffers a program wrote
flip after it, flip.* keys and deferred_pre/composite_pre apply, and
the mapping carries over to the next frame. Clears at the start of the
frame with the pack's colours or the defaults.
- noisetex (256x256 white noise, repeat), a lit stand-in behind shadow
samplers until shadow maps exist, a 1x1 stand-in for other names; the
frame uniforms of the previous commit and the translator's own block.
- final draws onto the window; without it colortex0 is shown.
- Pipelines are built through validation scopes: a program wgpu refuses
is reported with the full error and skipped. The dimension's folder is
reloaded when the player changes dimension.
Nothing changes without the environment variable: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its own workspace, as the telemetry server's, for its own Railway service.
GitHub sign-in with no scope (the user's token reads the login once and is
dropped), signed-cookie sessions, an origin check and a CSRF token, six
submissions per account and sixty in all per hour. A submission runs the
file's own checks (horizon_mods::index::inspect, the signature, the entry's
fields through registry::project), uploads it to the registry's `staging`
release as <sha256>.<extension>, writes mods/<id>.toml on a new branch with
the submitter as co-author, and opens the pull request. Only an entry's
owners publish updates; a key change is a pull request by hand. Shader packs
are refused until their format exists.
Tested end to end against a mock GitHub: sign-in, the signed sample mod,
what lands on GitHub, and what is refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- `horizon_mods::index::registry`: the registry repository's
`mods/<id>.toml` and `revoked.toml`, `build` (every entry and file
checked, every problem listed, the index written) and `review` (a
published file never changes or disappears, a kind never changes;
non-owner, owner and key changes pointed out).
- `cargo horizon-mod registry build|review|check|list` for the registry's CI.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- `horizon_mods::index::inspect`: each kind's own check (a mod's manifest
and content, a modpack's manifest and overrides, a resource pack's
pack.mcmeta), returning what the file says; `check_file` now compares
those facts with the index. Shader packs and unknown kinds are refused,
and `index::open` says which kinds the registry takes: shader
submissions stay closed until Horizon's own format exists.
- `cargo horizon-mod check <file>`: those checks plus a trial run of a mod
in the sandbox (ticks, frames, a chat line, each command), text or JSON.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The values a pack's post-processing programs read once per frame, in
GL's conventions: camera position and the previous one, eye altitude,
up/sun/moon/shadow-light positions in view space (the sky's own frame for
the sun and moon), sun and shadow angles, world time, day and moon phase,
frame counter, frame time and run time, view size and aspect, near and
far, rain, smoothed wetness, thunder, fog colour, start and end, sky
colour, eye in fluid, blindness, night vision, darkness, brightness, GUI
hidden, eye brightness and its smoothed value, and the model-view and
projection matrices with their inverses and last frame's. The projection
is GL's perspective with the world's near and far planes times vanilla's
view bobbing. Each value is written as the type the pack declared.
The game passes what only it knows (day time, weather levels, the fluid
at the camera, light at the camera, blindness, F1, brightness) in the
renderer's Frame when HORIZON_SHADER_PACK is set.
Fix (slice 4): programs that draw to buffers read gl_FragCoord through a
variable only final set; every target sets it now, with a test. Found by
the first frame drawn with a pack.
Nothing reads the uniforms yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
As a client (multiplayer, a LAN guest, single player on its server
thread) Horizon ran its own fall damage, fire, freezing, berry bushes and
starvation through the local hurt, so the camera's hurt tilt started even
when the server cancelled the damage (lobbies that turn fall damage off).
Vanilla's client never hurts itself: Entity.hurt does nothing outside a
ServerLevel and FoodData.tick is ServerPlayer's. Its hurt animation comes
from the server: LocalPlayer.hurtTo on a health drop, and
ClientboundDamageEventPacket / ClientboundHurtAnimationPacket for itself.
- PlayerMotion.client_side: hurt_blocking and food_tick do nothing;
landing sounds and fall-distance tracking still run.
- remote: SetHealth flashes on a drop after the first health, and the
player's own damage events and hurt animations restart its hurt time.
- horizon-ctl state reports the player's hurt_time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both switched on past 4,096 section draws, measured when a section had
2.2 to 2.5 draws. A section's draw count now depends on the sides it shows
the eye, so the bars count sections in view: 1,600, where 4,096 draws fell.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
decorated_region compiles its oracle into chunk-generation-parity; when it
passed while chunk_generation was still running, the trim deleted
chunk_generation's dumps mid-run (FileNotFoundError on
*-987654321987-full.txt in CI on #1569, #1571 and #1584).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The frame loop skips Game::tick while the game is paused, so these never
fired; the server thread's paused pass is what moved the player on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla resolves a server with InetAddress.getByName, and Java's default
(java.net.preferIPv6Addresses=false) puts IPv4 addresses first. Horizon
tried addresses in the system resolver's order, which on macOS puts IPv6
first: a server whose IPv6 address resets the connection showed
"Connection reset by peer (os error 54)" where vanilla joined.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A registry modpack (.hpack) now shows under Modpacks with the Horizon
source, with its mods and what each may do, and installs into
horizon-packs/<id>/: the checked pack, pack.json and home/, where each of
its mods is downloaded and checked like any registry mod (pinned to the
pack's version and SHA-256) and turned on or off as the pack says. Its
overrides are copied on the first install only. A pack whose mod was taken
down installs nothing and leaves nothing behind. The Modpacks tab lists
installed registry packs with Play and Remove (confirmed); they play through
modpacks::apply_profile with their own HORIZON_HOME, taken-down mods turned
off first. CLI: `mods install --source horizon <pack>`, and `modpack
list|play|remove` see registry packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The source switch now picks Modrinth or Horizon (CurseForge stays coming
soon). On Horizon each result says whether its files are signed and what
the mod may do, in the game's words, before Install; no Java check, no
download count, no Modrinth filters. Installed Horizon mods get an
on/off switch (horizon-mods.txt); one the registry took down is turned
off when the page reads the index and can't be turned back on.
Registry icons load from its GitHub Pages. HORIZON_LAUNCHER_SOURCE=horizon
opens the page on it, for screenshots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mods install into horizon-mods/<id>.hmod (turned on) and resource packs into
resourcepacks/, each downloaded to mod-browser/downloads/ and checked by
horizon_mods::index::check_file (size, SHA-256, signature, manifest) before it
is moved into place. Files the index takes down are turned off when the index
is read or the game starts, and can't be installed or turned back on. CLI (and
so MCP): --source horizon, mods enable|disable, and search shows whether a
project is signed and what a mod may do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
horizon_mods::index (feature index): index.json's types and rules (unique
ids, https URLs, plain file names, hex SHA-256, minisign keys and
signatures, a modpack's mods present in the index), open-ended kinds
(mod, modpack, resourcepack, shader; unknown kinds kept, not offered),
revocations, and check_file: size, hash, revocation, signature and the
file's own manifest against its entry. horizon_mods::modpack reads
.hpack files: horizon-modpack.toml and an allow-list of overrides.
docs/api/mod-index.md documents both, with a sample index the tests read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decode the trim component as its material and pattern registry ids when both
are references (a direct holder stays read past) and name them through the
server's synced trim_material and trim_pattern registries, as enchantments
are, so worn and held trimmed armour draws its trim in multiplayer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's custom advancements often name themselves with an empty text and
the words in its siblings ({"text":"","extra":[...]}); only the top-level
text or translate key was read, so their tooltips were blank. Read them as
the component they are (ComponentSerialization), with siblings, styles,
translation arguments and fallbacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It reads the same files the same way (assets/pack.mcmeta's language
section, a language offered only with its file, Language::load over
en_us). Leaving the client out also keeps this PR to horizon-prepare, so
its check doesn't rerun the client's harnesses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- predict_use and predict_place set their blocks through one
predict_blocks (retainKnownServerState), instead of two copies of the
loop.
- remote_can_place returns where BlockItem.place put the block, and the
placer hears the place sound there: before, set_block only played it at
the clicked face's neighbour, so a block that replaced grass or snow
where the player clicked made no sound for its placer now that the
server leaves the placer out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A frozen level (TickRateManager.isEntityFrozen) ticks only the players, so
their fluid contact is the only one that moves on, as in the loop below.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Measured on the built branch: it does not stop the drowning from #1592 on
its own. Kept because it is what forcing the level to stand still looks
like, and the negative result is in the PR.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The window's own tick doesn't run while paused (the frame loop skips
Game::tick), so the two gates added there never fired. What kept going was
the server thread: its paused pass still ran tick_guests, the players'
doTick, so air, hunger and drowning damage moved on behind the pause menu.
Vanilla's IntegratedServer.tickPaused only ticks the connections, and
ServerGamePacketListenerImpl.tick skips tickPlayer while the server is
paused. tick_lan now does the same: the paused pass keeps the connections
and skips tick_guests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Evaluate four same-cell Y samples together while preserving coordinate
setup, gradient caching and each sample's floating-point operation order.
Safe arrays let LLVM vectorize the interpolation on the portable target;
cell boundaries and tails keep scalar evaluation.
Add bitwise differential coverage against the original volume loop.
Two paired chunk-generation batches reproduce a 4.6–4.7% Nether wall-time
reduction, with smaller or unresolved effects in the other dimensions.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
What the renderer needs to run a pack's post-processing, still without
wgpu: the programs of one dimension (prepare, deferred, composite and
final with their numbered variants, in the order they run), each
preprocessed and translated, with the buffers it writes and the flips
that follow it, and the buffer settings its stage files carry.
- A dimension's folder (world0, world-1, world1) replaces the base folder
when it exists. program.<name>.enabled expressions over switch options,
flip.<program>.<buffer> keys and the virtual deferred_pre and
composite_pre. Programs with a geometry stage, or that fail to
preprocess or translate, are reported and skipped.
- tests/packs/post-test: Horizon's own small test pack (tint, depth in
colortex2, depth bands on half the screen, vignette).
Black box over the public test packs: every dimension of every pack
loads its post-processing programs (2 to 9) with no note.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The configuration packs write into their stage files, read from live code
only (the preprocessor has dropped false branches, so an option's choice
is the one in effect):
- the buffers a fragment shader writes: the last live DRAWBUFFERS or
RENDERTARGETS comment, at most 8, buffers 0-7 without one;
- per colortex buffer (names and aliases): <buffer>Format in block
comments, with every format name of the spec in any case, as channels,
bits and kind; <buffer>Clear, ClearColor and MipmapEnabled.
A later file changing a setting, an unknown format and a value of the
wrong type are reported with their file and line. Buffer 1 becomes
RGBA32F when a program declares gdepth and the pack set no format.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in with GitHub through the registry's service (no scope), then send
the file and its page; the service checks the file, stages it and opens the
pull request, and the page links to it or says what to fix. A file named
<id>-<version>.<ext> fills in its kind, id and version. Shader packs are
listed and disabled until Horizon's own format exists. Caddy sends
/api/mods/* to REGISTRY_UPSTREAM (unset: the form says it isn't open), and
"Submit yours" now leads here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- AbstractBoat.push(player) moves the player too: the boat's own tick and
the player's pushEntities run on the player's client in vanilla
(pushableBy lets a client push only its local player), so the window's
player is pushed in-frame and a server's client pushes itself in
remote_pushes, as it does for mobs.
- A mob the boat would take that can't get in (boarding cooldown, boat
under water) is left alone, not pushed: the tick calls startRiding,
which fails quietly, instead of push.
- Mob.startRiding drops the mob's lead.
- look_at_entity reuses the guest's eye position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`m.fluids` is filled by `Mob::update_fluids`, which only runs in the mob tick
on the server. A window that is a client of a server never runs that tick, so
every mob it draws kept the default `Fluids { in_water: false }` for its whole
life. Cod, salmon and tropical fish rotate onto their side while `!in_water`
(`mobs/aquatic.rs`), so they were drawn lying on their flank in water, while
their position and heading, which come from the server, looked correct (#1591).
`tick_remote_entities` now runs vanilla's `EntityFluidInteraction.update` for
the displayed mobs before it ticks their animations, so the renderers read the
water contact the same way they do for a mob the window simulates itself.
Fixes#1591.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Air kept draining behind the Escape menu in singleplayer and the drowning
damage landed on closing it. The server pauses (`IntegratedServer::set_paused`
reads the flag), but the window's own copy of the player kept running
`controlled_simulation_tick` every frame, and that is where
`tick_environment_damage` decrements the air and pushes `MotionEvent::Drown`.
`Minecraft.tick` skips the level while the game is paused, so both local paths
now skip the player's simulation: the client of a server (`tick_remote_level`)
and in-frame single player. Keys are already neutral when a screen is open, so
this only stops the environment, hunger and fall prediction from running ahead
of the paused server.
Fixes#1592.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
#1570 landed the client pack manager without vanilla's
PackDownloadNotificationComponent: the SystemToast that follows a
download with download.pack.progress.percent or .bytes and stays as
download.pack.failed when a batch fails. The download queue reports
requestStart/downloadedBytes/downloadFinished on a channel (vanilla's
DownloadProgressListener), the game ticks them into a toast it slides
in for the vanilla 5 seconds with the toast in/out sounds.
Co-Authored-By: Devin <[email protected]>
The language screen offered English alone. The client jar holds only
`en_us.json` and `deprecated.json` under `assets/minecraft/lang`, and the list
the screen shows comes from `pack.mcmeta`'s `language` section, which the jar
does not carry at all. Both live in the asset index, so `horizon-prepare
languages` copies `pack.mcmeta` and every `minecraft/lang/*.json` from the
asset store the way the sounds, the panorama and Unifont already are, and full
preparation runs it with the rest.
Every bundle the client declares is then offered, `Русский` among them, and
`lang` in options.txt selects one (#1272).
Fixes#1272.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
BlockItem.place plays it with playSound(player, ...): the server sends it to
the others, and the placer's client plays it as its block appears. The
server sent it to the placer too, so each block sounded twice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A program's preprocessed GLSL, any version from 110 to 460, compatibility
or core, rewritten into the Vulkan-flavoured GLSL 450 that naga's frontend
reads, then parsed and validated into naga modules with the program's
interface: its uniform block (offsets from naga's layout), its samplers
and their bindings, its output locations. The rewrite works on
preprocessing tokens and keeps the pack's lines, so every error points at
the pack's own file and line through the preprocessor's line map.
- Loose uniforms go into one block shared by both stages; bool is stored
as int. Samplers are split into texture and sampler objects, combined
again where used, and passed as two arguments to the pack's functions.
- The compatibility built-ins become declarations of ours (prefix hz_):
gl_Vertex, gl_MultiTexCoordN, the matrices and ftransform(), gl_FragData,
gl_FragColor, gl_TexCoord, gl_FrontColor, gl_Fog (from vanilla's fog),
texture2D and the other old texture functions, shadow2D; varyings get
locations by name across the stages; words that became keywords after
GLSL 1.x are renamed in old code.
- naga gaps covered: modf, conditional initializers of globals (run by our
main), and unsized arrays sized from their constructor.
- The pack's main is called by one of ours that draws the quad from (0,0)
to (1,1) and adapts GL's conventions: clip y negated for every target but
the window, so memory rows are GL's rows and gl_FragCoord needs no change;
clip z from GL's [-w, w] to [0, w], so stored depth is GL's window depth.
In final, gl_FragCoord.y is counted from the bottom of the window.
Black box over the public test packs: all 105 composite-style programs
(composite, deferred, final, prepare, begin, shadowcomp of the base and
dimension folders) translate and validate.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's ServerRedirectHandler asks through JNDI's DNS client: the
system's name servers in turn, one 1 s UDP try each (retries = 1), TCP when
the reply is truncated, the first SRV record of the answer. resolver/dns.rs
does that by hand in ~200 lines with wire-format tests and a local stub
server, so the game, the dedicated server and the bots no longer pull in
tokio, hickory and ~25 other crates (none of them were in the game before).
Name servers come from resolv.conf on Unix, as JNDI reads them, and from
the network adapters on Windows (ipconfig, Windows only; the crate keeps
forbid(unsafe_code)). Each server waits 1 s, as vanilla does, instead of
the whole connect timeout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The clientbound boss_event packet (add/update/remove/style/progress), a
BossOverlay that renders each bar, darkens the world under a wither, and
suppresses the create-the-world fog — vanilla's BossHealthOverlay and
GameRenderer behaviour on a real server.
Co-Authored-By: Devin <[email protected]>
The list (search, a filter per kind) and a page per project: what a mod
may do in the game's own words, Signed or Unsigned, how to install it
from the launcher, the CLI or by hand, its file, SHA-256 and versions,
and links to its source, its registry entry and the report form. Baked
from the published index at build time (HORIZON_MOD_INDEX for a test
index) and refreshed by the page; Caddy serves /mods/<id> from its baked
page, else the list page, which shows a project published since.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Meshing sorts each section's and distant tile's solid quads into seven
buckets: one per axis direction (all four corners in one axis plane, both
triangles wound the same way) and one for everything else, and keeps each
axis bucket's extreme plane. A quad facing +X at x = p is a back face from
an eye at x <= p, which the solid pipeline culls anyway, so the frame draws
only the buckets that can face the eye, merged into runs of adjacent
buckets: one to three indirect draws per section on the existing page-run,
occlusion-culling and flat paths. The eye comes from the frame's
view-projection, so view bobbing is exact. The picture does not change;
`horizon-ctl section-sides on|off` switches it for A/B checks.
Written from scratch from the general back-face technique; no other
renderer's code or Horizon's removed code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A stage file goes through comment removal, #include, conditionals and
macro expansion, and every output line remembers the pack file and line it
came from, so later errors point at the pack's own code.
- #include "x" from the including file's folder, "/x" from the shaders
folder, expanded in live code only (an include inside a false #if is not
read), ten levels at most, paths that leave the pack refused; a file is
read once per run however often it is included.
- #define, #undef, line continuations, __LINE__ and __VERSION__; #version
and #extension kept apart for the translation's own header; the block
comments of live code kept for the directives packs write in them
(DRAWBUFFERS, buffer formats); #error and unknown directives diagnosed.
Black box over the public test packs: every stage file of the base and
dimension folders (126 to 219 per pack) preprocesses with no diagnostic.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The macro engine the GLSL preprocessor needs, shared with the .properties
reader: function-like macros take their arguments (expanded first, raw
beside ##, which pastes) when a ( follows their name; a macro is still not
expanded inside its own expansion. Text is cut into preprocessing tokens,
so numbers such as 1e5 or 0x1F are never mistaken for names.
The #if/#elif/#else/#endif state moves out of the .properties reader so
the preprocessor uses the same code.
Standard macros: MC_GL_VERSION and MC_GLSL_VERSION 460, the 24
MC_RENDER_STAGE_* as ascending integers in draw order, render and shadow
quality 1.0, MC_HAND_DEPTH 0.125, MAX_COLOR_BUFFERS 16.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.useItemOn places the block on the client's level at
once, kept under the use's sequence until BlockChangedAck. Without it the
next fast click aimed past the block still on its way from the server, the
server refused it, and the arm swung for nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/api/mods.md gets "Ways in": one API (the WIT world) and one sandbox
(WebAssembly) for every language. Rust is first-class; Lua scripts are
planned, run by an interpreter that is itself a WebAssembly mod linked with
the script's capabilities only, with hot reload of the script; C, C++, Zig,
TinyGo, AssemblyScript and JavaScript/TypeScript are to be evaluated, each
listed with its cost measured against Rust.
Server mods are their own line (api 0.3); block entities are no longer
tied to a draft. Promotion to stable: the orchestrator proposes with the
evidence, Ludovic approves.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/api/ gets one page per surface other programs build on (mods,
shaders, agent control, servers, the launcher, data formats), each with an
overview measured from the source, the guide, the generated reference, examples,
its stability, what is not there yet and a changelog, plus the policy every
API follows (docs/api/stability.md): a version per API, semver lines,
experimental / preview / stable, deprecation windows, compatibility tests and
the level of every part today.
The website gets a "Build on Horizon" page (website/docs/apis.md) that
summarises them and links the repository's pages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A moving boat now stops at other boats, minecarts, mobs and players
(AbstractBoat.canVehicleCollide), and on the server it pushes what it
touches and is pushed back (AbstractBoat.push, LivingEntity.pushEntities).
A boat no player steers picks up the mobs that touch it, two seats in a
boat and one in a chest boat, with the player always in front
(getPassengerAttachmentPoint, clampRotation). Mobs get out when the boat
breaks or stays under water, are saved under the boat's Passengers, and
are seated by the client, which is sent no passenger's position.
A boat can't be placed where it would overlap an entity (BoatItem.use's
noCollision), and the player sits at the raft's and chest boat's vanilla
seat. A server player breaking a boat with a mob in it hits the boat.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/api/server-reference.md lists horizon-server's commands (its help
text), every server.toml key (config::TEMPLATE: default, the
server.properties key it writes, what it does) and the control API's
endpoints. Tests fail when the page is out of date (HORIZON_WRITE_DOCS=1
rewrites it), when the template and Config disagree on the keys, when a key
has no comment, or when api::ROUTES and the endpoints route() answers
differ.
api::ROUTES replaces the endpoint list in api.rs's module comment and the
hand-kept path list of the 405 arm. The template's uncommented keys get a
comment (what init writes for new servers; no setting changes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
shaders.properties and the id files (block, item, entity, dimension):
key=value lines in file order with their line numbers, # comments, a value
continued on the next line by a trailing backslash, and the conditional
directives evaluated with the standard and option macros. A # line is a
directive only when its first word is one, so a commented-out key such as
#ifdefault=1 stays a comment. #define and #undef apply to the lines after
them in every one of these files: a public pack defines a macro inside
#ifdef MC_OS_MAC in its shaders.properties and tests it further down. A
later duplicate key replaces the earlier one; broken conditionals, lines
that are not key=value and duplicates are kept as notes for the pack's log.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The integer arithmetic of #if/#elif that a pack's .properties files and,
later, its GLSL preprocessor share: defined X and defined(X), decimal,
hexadecimal and octal literals, the C operators with C precedence, macros
expanded to their values (a macro is not expanded inside its own
expansion), and an identifier that is not a macro read as 0. A bad
condition says why. The standard macros that need no renderer:
MC_VERSION, from the pinned version by the documented rule (26.3 is
260300), and the system's MC_OS_*.
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new crate, horizon-shaders, the first piece of shader pack support written
again from a specification of the format. It opens a pack (a folder or a zip
whose root holds a shaders folder; decoy folders and files beside it are
ignored), lists the packs in a shaderpacks folder with the reason a broken
one can't be used, and reads a pack's lang files: English under the player's
locale, file names matched ignoring case (packs ship en_US.lang).
Nothing uses it yet: players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- `Span::Space` kept the range of rows it hid, written and never read: it is just a height.
- The check thread sent the project page's verdict from inside a `map` closure; it is now an
explicit step before the result goes to the results list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/api/mods-reference.md lists every capability, world, interface, type,
function and hook of horizon:mods, generated from
crates/horizon-mod-sdk/wit/horizon.wit and the manifest's capabilities by a
test in horizon-mods (feature pack, as CI runs it). The test fails when the
page is out of date (HORIZON_WRITE_DOCS=1 rewrites it), when a function has
no doc comment, or when a capability grants an interface the contract lacks.
The WIT's undocumented functions and fields get doc comments (no change to
the contract itself).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The helper's release check repeated the `pressed &&` guard below it, and
its test only covered the helper. The comment now says why text screens
take presses first: vanilla types through charTyped, which debug keys
cannot swallow, while here the character comes with the key press.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- horizon-bench reads the machine with horizon-telemetry's `command_output` (now public) instead
of a second copy of it; the console flag lives in one place.
- horizon-server's `process::command` hides the console only when the server has none (started
by the launcher). Run from a terminal, `schtasks`, `tar` and `mklink` share it as before, so
their output still reaches it: a CREATE_NO_WINDOW child can't write to its parent's console.
- horizon-prepare's java/javac keep the stdin they had; only the console flag is new.
- launcher `process::hide_console` returns the command (no `let _ =` on other systems); the
machine-details thread failing just leaves the report without them, as a slow one does.
- README: the Windows subsystem's effect on the command line (interactive cmd/PowerShell don't
wait or set the exit code) under "Command line", the deferred probe under "Performance log".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A resize re-baked Home's landscape every 6 px and never removed the old images from the
window's sprite atlas (GPUI keeps an image there until `drop_image`): dragging a window wider
piled up textures of up to ~14 MB each. The landscape now grows in steps of 64 texels, is kept
when the window narrows, and the one it replaces leaves the atlas on the next frame.
- The landscape was laid out from the width (sun at 0.68 w, one random stream for every part), so
it reshuffled hills, trees and the sun at each step of a resize. Each part now has its own
stream drawn left to right: a wider landscape only adds columns.
- macOS and Linux keep their system title bar, so the top bar keeps the mark and "HORIZON"
there (docs/design-system.md, Navigation); only the Windows title bar carries them.
- The before/after screenshots were for the pull request, not the launcher's docs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon decoded 25 of 26.3's 122 data component types. Item data has no
length prefix, so a stack with any other component (an armour trim,
banner patterns, pot sherds, a stew's effects, a plugin's item) made the
whole packet undecodable and it was dropped: on a Paper server the
inventory arrived empty until each slot was clicked, the advancements
screen said "There doesn't seem to be anything here..." and the recipe
book was empty.
Every other component is now read to its end with its vanilla
StreamCodec (item/skip.rs) and kept as its bytes, written back as they
came; components without a network codec go as their NBT. The client
leaves them out of the item's components, so the item keeps its
prototype's values for them.
tools/test_item_wire_parity.py checks it against vanilla: the oracle
encodes 374 stacks with ItemStack.OPTIONAL_STREAM_CODEC (every default
component value of every item, SNBT fixtures for the rest, direct
holders and removals built in Java), covering all 122 component types,
and the Rust probe must consume every byte, find the same component ids
and encode the read-past ones back to the same bytes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PackSelectionScreen.loadPackIcon registers every pack's pack.png as a
texture; Horizon never loaded them, so every pack (Default, Programmer Art,
High Contrast and the player's own) fell back to unknown_pack, the
cobblestone. The icons now load from each pack when the screen opens and are
written into 64 new 128x128 HUD atlas slots, one per icon on screen. The
vanilla pack's icon is the client JAR's pack.png, which horizon-prepare now
copies into the prepared resources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The comment names vanilla's rule (LevelChunk.setBlockState keeps or creates
the block entity when the block, not the state, is still there after
onPlace). The corpus checks the `enabled=false` flip, drops the cleanup lines
the group's closing /fill already does, and adds a powered hopper placed by
/fill, which goes through the same place_state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Collect performance-report hardware after the first frame and preserve logs, input and CLI pipes while suppressing background terminal windows.
Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
Keep page content and pixel art responsive, align navigation left, and use native Windows control hit regions in themed chrome.
Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
/setblock created the block entity only when the state that landed matched the
requested state exactly. HopperBlock.onPlace sets enabled=false while the block is
being placed, so a hopper put into power had no inventory: /data get block answered
"not a block entity", items dropped into it stayed item entities, and
/setblock hopper{Items:[...]} dropped the NBT as well.
Compare the block at the position with the block that was placed, the way vanilla
does when it creates the entity from the placed state, before the neighbour updates
run.
Fixes#1506.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The game logs into HORIZON_HOME and falls back to the player's own game folder when it is unset. Harnesses
that set only HORIZON_NO_AUDIO (and test_native_client, which set nothing) wrote logs, and on a world run
advancements, into ~/Library/Application Support/horizon.
lab.game_env gives every game a fresh HORIZON_HOME, HORIZON_LAUNCHER_HOME, XDG_DATA_HOME and XDG_CONFIG_HOME,
no audio and no launcher self-update; a folder the caller passes is kept. Every launcher of the game uses it,
parity.py gives each harness its own HORIZON_HOME, and tests/test_game_home.py fails for a script in tools/
that starts the game without it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Started as a bare executable, the game showed in the Dock and Cmd+Tab as
"horizon-client" with the generic executable icon: winit's window icon does
nothing on macOS. The launcher now keeps a small Horizon.app in each version
folder (Info.plist, the launcher's icon, hard links to the version's
horizon-client and horizon-ctl) and starts the game from it. Server setup
leaves the bundle out of its copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Servers that push a resource pack now work (reported by scanfee on Discord:
"Servers requiring custom resource packs do not work yet"; net.rs declined
every push).
- net.rs forwards resource_pack_push and _pop in configuration and play, and
answers in the phase the connection is in.
- The prompt is vanilla's PackConfirmScreen on a shared ConfirmScreen
(confirm_screen.rs, now also used by the game rules screen): the required
or optional wording, the server's message, Proceed/Disconnect or Yes/No,
Escape for no. Declining a required pack disconnects with vanilla's reason.
- The server list's Edit screen has the Server Resource Packs cycle button
(Enabled, Disabled, Prompt), saved as acceptTextures; the prompt's answer
is saved there too.
- Downloaded packs go on top of the player's packs, in push order, through
the same reload as the Resource Packs screen; they are removed on pop and
when leaving the server.
tools/server_pack_e2e.py drives the real client against vanilla's dedicated
server or Horizon's, with a test pack (blue dirt, lang strings) from a local
HTTP server, and checks the statuses the client sends through net_capture:
decline, proceed, the cache, a wrong SHA-1, an optional pack declined and
accepted, a dead URL.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's ServerPackManager, DownloadedPackSource and DownloadQueue, without
the game around them yet: the prompt state, each pack's download and
activation, the statuses in vanilla's order (onSuccess repeats
SUCCESSFULLY_LOADED, a failed reload recovers with the packs that loaded),
pop, and leaving.
Downloads follow HttpUtil.downloadFile: downloads/<pack id>/<sha1>, a cached
file whose hash matches is reused, a wrong hash fails, no hash downloads to a
temporary file renamed to its SHA-1, 250 MiB at most, vanilla's X-Minecraft-*
headers, a log.json entry per download, and 20 files kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#503 grouped each section's solid quads by the axis side they face and
skipped the groups facing away from the eye. Its code and PR named an
internal class of another project as the model. Horizon doesn't build on
another project's code, so the grouping goes and every section draws its
solid quads in one draw, as vanilla does; the GPU's back-face culling
discards the back faces, so the picture is unchanged.
This costs frame time, measured on the Mac mini in the PR. A clean-room
version can come back later, designed from vanilla and from first
principles only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerResourcePackConfigurationTask: after the tags, the configuration phase
sends the pack (id from the URL when none is given, hash, required, prompt)
and waits for a final answer, with keepalives every 15 s. A declined required
pack disconnects with multiplayer.requiredTexturePrompt.disconnect, in
configuration or in play, with vanilla's log lines.
tools/test_server_resource_pack_parity.py drives a vanilla 26.3 server and
Horizon's with the same five players (declines, accepts then declines in
play, fails the download, answers after 20 s, declines an optional pack) and
compares what each one receives.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A developer of Iris showed that Horizon's shader pack code was written from
Iris's source. This removes it: the horizon-shaderpack crate, Java pack
loading and its render stages, Horizon's own WGSL packs and the example
packs, the Shader Packs screen and its option, the launcher's shader pack
browsing and installing, and the docs about them. Shader packs may come
back later as a clean-room rewrite.
The vanilla renderer and its post-processing (resolution scale, FXAA) are
unchanged. Options files that name a pack load and drop the key; the
launcher reads manifests that list a shader pack, and files in
`shaderpacks/` are never deleted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VillagerProfessionLayer takes a baby's type texture from
`entity/villager/baby/<type>` (and `entity/zombie_villager/baby/<type>`),
not `type/baby/<type>`: that path names no texture, so baby villagers and
baby zombie villagers of every biome drew without their type layer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) never read Villager.DATA_VILLAGER_DATA or
ZombieVillager's from SetEntityData, so every villager drew as a plains
villager with no profession and no level badge, and a villager that took a
job never changed. The client now reads the type and the profession by
their built-in registry ids, and the level, which VillagerRenderer reads
from the synched data in vanilla.
horizon-ctl entities reports a villager's type, profession and level as the
game knows them, so a client's view can be checked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The opt-in Cull Leaves setting (#420, from #387) was adapted from the
CullLeaves mod's code (MIT, credited in THIRD_PARTY_NOTICES.md). Horizon
doesn't build on another mod's code, so it goes, with its notice.
Leaves now follow vanilla only: with opaque leaves (cutoutLeaves off) the
faces between two leaf blocks are skipped (LeavesBlock.skipRendering),
as before #420 and as the setting's Off did. Mangrove roots keep every
face. An old options file's cullLeaves line is ignored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve Java parity oracles and comparison benchmark instrumentation. Move their remaining dependency locks under tools/benchmark-deps and remove legacy product packaging, fixtures, and CI checks.
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
The launcher had no app menu and no key bindings, so Cmd+Q did nothing.
Add Quit and CloseWindow actions, bind them to cmd-q and cmd-w on macOS,
and set an app menu with "Quit Horizon Launcher" and a Window menu with
"Close Window". Quit shares one shut_down path with closing the window
(stop the servers this launcher started, send telemetry, exit), so a
running game is treated exactly as it is today.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Removes docs/performance/java-perf-mods.md (a survey of eight Java
performance mods' techniques) and docs/research/java-mod-compat.md (notes
on how Fabric, NeoForge and Mixin work, with a list of mods to port).
Links to them now point at mods.md, which says in two lines why Java jars
don't run in Horizon.
docs/modpacks.md no longer lists a mod's decompiled code as an input of a
port: a mod's code is never decompiled, read, quoted or paraphrased, and
ports are written from the mod's observed behaviour (the oracle and the
harness), its own data files and its public documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Walking through the cart pushes it back once past its middle, so the
check stands the player half into the cart from the north and waits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One release per merge made about 40 releases in 31 hours, hard for players to follow (asked on Discord).
release-now still releases at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lan_e2e's vehicles check: a creative break whose block is put back stays,
a player stands on a boat, lava breaks a ridden boat and lets the rider
out, a minecart in lava breaks and a TNT one goes off, and walking into a
minecart pushes it, for the host and the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a LAN guest or any remote client, getblock and container read what
that client has again (test_friends waits on a guest's getblock). On the
server thread the block commands run in the owner's level, not whichever
level the server ticked last.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractBoat.tick runs applyEffectsFromBlocks twice, and a minecart takes
lavaHurt from its behaviour's applyEffectsFromBlocks and again from its
own isInLava check: two 40-point hurts a tick, so lava breaks either on
its first tick inside (a TNT minecart primes instead). A minecart has no
baseTick and a boat never outlives lava, so the fire countdown is gone.
Minecart pushing takes the player's own box and returns at once when
there are no carts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Guests on the server thread (and LAN) now push carts with vanilla's push
math. The push back on the player is dropped in both modes: vanilla only
syncs it to other players, so the pusher's own client never feels it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vehicles take lava damage and keep burning for 15 seconds like vanilla
(lavaHurt, remainingFireTicks), breaking and dropping their items; a TNT
minecart primes from fire. Boat placement stops at lava as well as water.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Boats join happy ghasts in the player's entity collision list, so a
player can't walk through a boat and steps up onto it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the integrated server thread on, setblock, fill, getblock and
container ran on the client's copy only. They now run on the server, and
on a remote server setblock and fill go out as chat commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A creative START destroys the block at once and its client never sends
STOP, so the kept dig broke whatever was placed in that cell next, every
destroy delay, until the player broke something else.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.causeExtraKnockback pushes along Mth.sin/Mth.cos of the attacker's
yaw, which come from vanilla's 65536-entry table: facing east, the push
has no z at all. Horizon took f32 sin/cos, which left -4.4e-8 of z per
block of push (a sprinting or Knockback hit, on mobs and players).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.causeExtraKnockback stops the attacker's sprint on the server, and
the shared flag reaches the attacker's own client (ServerEntity sends a
player its own dirty entity data). A guest's sprint was only stopped on
the server copy for the length of the hit and never sent, so a guest (and,
on the server thread, the window) kept sprinting: every hit while
sprinting had the sprint knockback, where vanilla's needs the sprint to
start again.
- leave_guest keeps the sprint stop a guest's hit made; a guest hitting a
player stops its sprint too.
- The hub sends each guest its own shared flags (crouching, sprinting,
gliding) when they change.
- A remote client takes the sprinting bit of its own shared flags (the
flag only, as vanilla's assignValues: the speed modifier and the
wasSprinting it compares stay, so it sends START_SPRINTING again only
after it has stopped).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's (and, on the server thread, the window's) knockback started from
the velocity the last hit left on its server copy, which nothing ever
slowed down, and each hit went out at once. So every hit after the first
pushed harder (0.72, then 0.76 blocks a tick where vanilla sends 0.4 each
time), netherite resisted from the wrong base, and a zombie's hit reached
the client before the friction of the tick it landed in.
Vanilla's server simulates a player too (MoveSimulationType
AUTHORITATIVE_SIDE_AND_SERVER): ServerPlayer travels every tick with no
input, and ServerEntity.sendChanges sends a velocity a hurt marked
(Entity.markHurt) as it is after that travel. A player's melee hit is the
exception (Player.causeExtraKnockback): it sends at once, then puts the
server copy back to its velocity from before the hit.
- MotionWorld::server_travel: Player.travel with no input, keeping only
the velocity (the client's moves place the player). The server's copy
checks no fall damage on its own moves (isLocalInstanceAuthoritative).
- guest_server_tick runs it last, then sends the velocity if a hurt
marked it since the last tick.
- guest_hit_by leaves a marked velocity to that; guest_struck_by (melee
from a player) sends at once and restores.
- lan_e2e `knockback`: three hits each way push the victim as far each
time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stable Clippy (1.99, what CI runs) does not flag q < -2. || q > 2., so the
helper and its test only served Rust 1.90's Clippy. Fire burnout is not
'intentionally disabled': vanilla's FireBlock.tick reads the
INCREASED_FIRE_BURNOUT environment attribute, set by eight overworld biomes,
which Horizon does not read yet; the comment now says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI runs stable Clippy (1.99), which is clean on main. The #[expect(clippy::boxed_local)]
in streaming.rs is unfulfilled there and fails CI under -D warnings, so it goes.
Also back out the rewrites that only satisfied Rust 1.90's Clippy and read
worse: the dragon's range helper and its test, the axolotl test's restated
formula, and two De Morgan expansions (first_person_probe, explosion).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 multiplies bobHurt/bobView into the projection
(GameRenderer.renderLevel); Iris's MixinModelViewBobbing moves them into
the model-view when a pack is in use. The comment said vanilla keeps the
bob in the PoseStack, which is Iris's behaviour, not vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/test_server_knockback_parity.py runs vanilla's dedicated server and
Horizon's (`--dedicated --nogui`) on one flat world and joins both with two
vanilla-codec players (KnockbackClient):
- Alice hits a cow and a zombie (plain, sprinting, Knockback II, critical,
in the air); the game is frozen around the hit and stepped a tick at a
time, and the mob's Motion and Pos over 20 ticks are compared exactly;
- Alice hits Bob (the same, a shield, netherite, two hits, two sprinting
hits, a mace smash, a stronger hit inside the hurt cooldown): Bob's own
motion packets, Alice's and her sprint flag are compared;
- a zombie hits Bob twice: his motion packets.
7 of 21 cases match on main. KNOWN lists the other 14 with their causes:
the guest's server-side velocity never travels (knockback adds up from hit
to hit), the server never sends a guest its own sprint flag, the extra
knockback's direction uses f32 sin/cos instead of Mth's table, a hit in
the hurt cooldown still knocks back, and a mace smash on a player runs
none of MaceItem.hurtEnemy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hit that lands while the player is still in its hurt cooldown, harder
than the last one, only hurts by the difference (LivingEntity.hurtServer):
it marks nothing and deals no default knockback. Horizon knocked the
player back 0.4 for it anyway, so a quick second hit (a weak swing, then a
charged one) pushed twice.
The hurt now reports whether its knockback hook ran (a fresh hit, not
fully blocked), and mob_hits_player knocks back only then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`/give @s wooden_sword[enchantments={knockback:2}]` (or an item saved
that way) kept the key `knockback`, which no enchantment lookup matched:
the sword showed no enchantment and knocked back like a plain one.
Vanilla reads the keys as Identifiers, so `knockback` is
`minecraft:knockback`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack rebuilds view positions from the depth buffer with
`inverse_projection` and then multiplies by `inverse_view`
(`gbufferModelViewInverse`), and Horizon's own packs do the same through
`world_offset` and `frame.inverse_view`. Passing
`world_projection(fov, aspect, far) * bob` as the projection while `view`
stayed unbobbed left the two halves disagreeing by the walk bob, so
everything a pack reconstructed — reflections, ambient occlusion,
godrays — turned with the bob. That reads as reflections wobbling in
step with the first-person hand, and changing the FOV made it worse.
Vanilla and Iris carry the bob in the world's pose stack (the model-view
matrix), not in the projection. Pass `bob * view` as the view and the
plain world projection instead: `projection * view` is unchanged, so
terrain, entities and the pack's own geometry programs draw as before,
while depth reconstruction is now consistent. The hand's uniforms
already carried the bob in its pose and are untouched.
Checked with Complementary Reimagined r5.9.3 over the depth-feedback
passes at 70 and 110 FOV.
Co-Authored-By: DeepSeek V4.1 Flash (OpenCode agent) <[email protected]>
horizon-client, horizon-world and horizon-protocol each had an example
named `probe`. Cargo copies every crate's examples into one
<profile>/examples/ directory, so parallel parity harnesses sharing a
target dir could run another crate's probe (CI parity run 40055656303:
command_parse ran the horizon-world dispatcher). The examples are now
client_probe, world_probe and protocol_probe ([[example]] in each
Cargo.toml; the files stay where they are), and every harness builds
and runs its own crate's name.
tools/parity.py recognises the new names when it counts the probes a
harness ran, and renames the old `probe` in recorded artifacts to the
crate's target (from the record's -p, else its probe sources), so the
prebuild still builds them instead of dropping them as unknown.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main. Single player runs on its server thread by default, as vanilla does; `--in-frame` brings back the old mode. Evidence: the smoke session passes 48/48 in both modes. ThinkPad (HD 620, Mesa) RD 16: 651 vs 185 FPS. M4 Pro, v0.2.25 release build, forest, offscreen, alternating runs: 1% lows 224–231 → 399–476 at RD 16 and 120–124 → 210–213 at RD 32; average FPS within 4% at RD 32.
Villager.spawnGolemIfNeeded set GOLEM_DETECTED_RECENTLY on the villagers
near a new golem, but Horizon wrote only villager::Ext, and the next brain
tick copied the empty memory back over it: the same villagers could summon
another golem before their sensor saw the first. golem_detected now sets the
brain memory (599 ticks).
IronGolem.checkSpawnObstruction checks the floor's top face (no golems on
dirt paths, farmland or slabs) and three blocks, the feet whatever fluid is
there; Horizon checked four blocks and no floor, so a roof three blocks up
kept golems out. The search's open block is air or a liquid block, not any
waterlogged one, and the villagers asked are those whose boxes meet the
caller's box grown by 10. The golem is created (yaw, follow range bonus,
left-handedness from the level random) before the obstruction check, as
trySpawnMob does.
The search and checks move to mobs::villager so the mob_ai probe runs the
game's code: four new scenarios (gossip, glass roof, dirt paths, panic) match
vanilla; with the old checks all four diverge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resources prepared before #1538, and every version that reused them, lack vanilla's built-in
packs, so players had to click Prepare again. The launcher now copies them itself, with
horizon-prepare's `packs` (the asset index's SHA-1 checked), when it starts, installs a version
or starts the game: only for prepared versions that lack a zip, once per launcher run. Without
them in any Minecraft folder's asset store it logs one line and the game starts as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (goldendays-1). Prepare copies vanilla's built-in Programmer Art and High Contrast packs from the player's own launcher asset store (SHA-1 checked; nothing ships with Horizon), so packs made to sit on Programmer Art, like Golden Days, show their classic textures.
Vanilla lists its built-in resource packs from minecraft:resourcepacks of
the vanilla pack (BuiltInPackSource.populatePackList), and those two zips
live only in the asset index, not in the client JAR. Preparation never
copied them, so Horizon's pack list had no Programmer Art. Packs made to sit
on it, like Golden Days ("This pack is meant to be put over Programmer
Art"), then showed the modern textures for the ~720 textures they leave to
it: furnaces, stone bricks, crafting tables, pumpkins, TNT, logs, tools,
buckets, paper.
Full preparation now imports both after extracting the JAR, like the
panorama and Unifont, and `horizon-prepare packs` adds them to prepared
resources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
PathNavigation.setRequiredPathLength calls updatePathfinderMaxVisitedNodes, so a
villager's PathFinder visits up to max(follow range, 48) * 16 = 768 nodes, not the
256 of its follow range alone. Horizon always used follow range * 16, so a villager
gave up on any job site, bed or bell whose path needed more than 256 nodes: in the
seed 12345 village it never found the way round to the mason's stonecutter door.
Navigation now keeps PathFinder.maxVisitedNodes: the base follow range times 16 when
the mob is made, then max(follow range, required path length) * 16 after
setRequiredPathLength and after any follow range change (Mob.onAttributeUpdated).
A happy ghast that grows up or becomes a baby gets a new navigation, as in vanilla.
mob_ai: villager_job_in_box, a composter inside a stone box whose doorway faces away
from the villager.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (discord-gate-1: player reports checked against vanilla 26.3; a lan_e2e check fails on main and passes here in both modes; evidence in the PR body).
InteractWithDoor was a stub, so a villager whose path went through a
closed oak door walked into it and stayed there. In generated villages,
villagers who were inside a house never reached a job site, a bed or
the meeting point.
Like vanilla, a villager now opens the closed mob-interactable doors on
its next path step (DoorBlock.setOpen: both halves, the door sound and
the block_open game event), remembers the door it leaves and the doors
it opened, and closes them once it is more than one step past them,
unless another villager within 2 blocks is walking through. A villager
going to sleep closes them too.
The mob events apply after the mob loop, so the door opens one tick
later than in vanilla, where DoorBlock.setOpen changes the world during
the villager's brain tick.
lan_e2e's new doors check shuts a villager in a room with a closed
door and a free composter outside. Before, it never opened the door;
now it opens it, becomes a farmer, and the door is closed again on the
server and on the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-2: server-thread gaps checked against vanilla, each with a lan_e2e check that fails before and passes after, host and guest, both modes; numbers in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
🤖 Agent: merged after verify and CI parity passed, built merged with current main. Code mods become horizon-client's `mods` feature: CI's two test commands run with `--features horizon-client/mods,horizon-mods/pack`; tests pass with and without it.
🤖 Agent: merged after verify and CI parity passed, built merged with current main (launcher-ship-1). #1521: generated docs keep LF line endings on Windows, so the launcher builds there again (dry run 40035909909 green on all three OSes). #1522: Prepare no longer requires Netty's optional native transport jar, which Prism never downloads (fixes#1487; test plus a real Prism-layout prepare).
🤖 Agent: merged after verify and CI parity passed, built merged with current main. The dedicated_server flake was the harness client never resending its position, so a villager whose chunk went out after its only move was never shown to it (0/21 → 21/21 with the server's tick thread stalled after the join). Failing harnesses now print the reason in CI.
🤖 Agent: merged after verify and CI parity passed, built merged with current main (reports-gate-2: each claim checked against vanilla 26.3, in-frame and --server-thread; evidence on the issue and in the PR body).
ServerPlayerGameMode breaks a digging player's block only when the
player's client says it is done (STOP_DESTROY_BLOCK, or the delayed
destroy), or at once when it is instant. The host ran the guest's
tick_mining with its own count and broke the block when that reached 1.
A guest whose client ran even a tick behind lost the block before it
predicted the break: no destroy particles, no break sound, and the
dropped item could appear before the block vanished.
lan_e2e's new digging check holds attack across six oak logs and hears
six breaks, then stalls the guest for a second halfway through stone:
the stone is still there on both sides when it resumes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st2: entity use with either hand and server chat components; lan_e2e checks pass in both modes. The hands check's only flake was a vanilla crit arrow killing the cow, fixed in c89695dd9: 40 of 40 runs pass).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (launcher-ship-1). #1521: generated docs keep LF line endings on Windows, so the launcher builds there again (dry run 40035909909 green on all three OSes). #1522: Prepare no longer requires Netty's optional native transport jar, which Prism never downloads (fixes#1487; test plus a real Prism-layout prepare).
The harness failed about 1 run in 3 on CI with "play-trade villager false" on
Horizon's side: the protocol client never saw the villager it trades with.
A server pairs a player with an entity only while the entity's chunk has been
sent to it, and checks again when either moves (ChunkMap.move / updatePlayers).
VanillaProtocolClient sent its position once, at the spawn teleport, and never
again. Horizon's first chunk batch goes out two ticks after the player is placed
(after the first tick's packets, as vanilla orders them); when a busy machine
held the tick thread back long enough for the client's move to be handled one
tick after placement, the villager's chunk wasn't sent yet, and with no later
move the villager was never paired.
The 26.3 client re-sends its position every 20 ticks standing still
(LocalPlayer.sendPosition, after a ServerboundClientTickEnd each tick). The
parity client now does the same from the time it is loaded until its death, so
an entity whose chunk comes late is paired at the next reminder, on either
server.
On failure the harness now adds the servers' and clients' warnings and errors to
its message and the client log's tail when the client fails, and parity.py's
summary prints the end of the first failing harnesses' logs, so CI shows why.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher.yml's Windows build has failed since the reference test landed: Git for Windows checks
text out with CRLF (core.autocrlf), so docs/launcher-commands.md never equalled what markdown()
generates. .gitattributes now pins LF for the generated files tests compare byte for byte, and
the launcher's check reads the file whatever line endings a clone gave it, with a test of a
CRLF checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Prism Launcher never downloads io.netty:netty-transport-native-kqueue (or the epoll one on
Linux), so preparing from a Prism install stopped on a jar the game runs without: vanilla's
EventLoopGroupHolder.remote takes KQueue/Epoll only when isAvailable(), else Netty NIO.
Preparing now imports a native transport when the launcher has it (checked against the
version JSON's sha1, as before) and goes on without it otherwise; every other library stays
required. Fixes#1487.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (reports-gate-2: each claim checked against vanilla 26.3, in-frame and --server-thread; evidence on the issue and in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st2: entity use with either hand and server chat components; lan_e2e checks pass in both modes. The hands check's only flake was a vanilla crit arrow killing the cow, fixed in c89695dd9: 40 of 40 runs pass).
#1428 made the in-frame world the default while a server's players lagged
behind it (signs, books, totems, keep_inventory and the other [server-thread]
issues). With the fixes of this stack a scripted single player session (walk,
mine, place, inventory, combat, eating, boats, minecarts, chests, furnaces,
beds, chat, signs, books, totems, keep_inventory, fishing, maps, saving and
reopening) gives the same results on the server thread as in the frame.
`--in-frame`, `--no-server-thread` or HORIZON_SERVER_THREAD=0 runs the world
in the frame; `--server-thread` still works. LAN guests, dedicated servers and
the command harness are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractContainerMenu.removed hands the stack a closing player holds on the
cursor to placeItemBackInInventory (dropped only what doesn't fit, or for a
player who left), and broadcastChanges then empties the client's cursor. The
LAN host threw it out at the player's eyes instead, for the inventory and
every container, crafting, station, mount and merchant menu, and the client
kept showing a copy on its cursor (#1283).
The three ContainerClose arms now place it back through lan_place_back and
send the inventory, carried stack included.
tools/lan_e2e.py gains a cursor check: each player picks up a stack in their
inventory and closes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lightning (Entity.thunderHit) and evoker fangs (EvokerFangs.dealDamageTo) hit
every entity in their box, but thunder_hit and fangs_bite only looked at the
window's own player, so a server's players (guests, and single player on the
server thread) were never struck (#1480, #1481). Both now run their player
part for each guest too, through Game::each_guest_hit.
The effects a mob gives the player it acts on (cave spider and bee poison, husk
hunger, wither skeleton wither, the nautilus's breath) and an axolotl's help
after a kill stayed with the server's own player: acts_on_player now routes
MobEvent::EffectPlayer and MobEvent::AxolotlSupportPlayer to the guest the mob
sees, like its blows (#1484, #1485).
tools/lan_e2e.py gains a strikes check: a bolt hurts each player standing in
water, and a cave spider poisons them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (guests, and single player on the server thread) the window ticks
its player through tick_remote_level, which copied the yaw, the Respiration
level, the wings and FAST_LAVA but not the rest of what the in-frame tick sets
from the window's own state. Both now call Game::sync_player_body:
- the look pitch: Player.travel steers sprint-swimming up and down by it (#1303)
- the slowdown of an item in use, LocalPlayer.modifyInput (#1278)
- leather boots, PowderSnowBlock.canEntityWalkOnPowderSnow (#1296)
- freeze-immune wear, LivingEntity.canFreeze
- the Auto Jump option, LocalPlayer.sendPosition (#1279)
tools/lan_e2e.py gains a body check: each player walks while drawing a bow,
stands on powder snow with leather boots and without, and sprint-swims looking
down.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed. A stopgap while in-frame is the default: an overloaded world slows down instead of dropping to 2 FPS (2,500 pigs: 1.7 → 16 FPS). It is removed with the in-frame path (FEATURES 4zg) once the server thread is the default again (#1463–#1466 and following).
A map a server's player (a LAN guest, or single player's window on its
server thread) made stayed blank and saved nothing (#1382): only the
game's own player ever updated a map, and no map data went to the guests.
Each guest's turn now runs MapItem's ticks as theirs (the marker of every
carried map, the reveal of the held ones), and the server sends each guest
ClientboundMapItemDataPacket for the maps it carries, and every 10 ticks
for the maps framed in the level (ServerPlayer.synchronizeSpecialItemUpdates,
ServerEntity.sendChanges).
As in vanilla, a map keeps its holders (MapItemSavedData.HoldingPlayer):
each has its own column sweep and its own record of what changed since its
last packet, so the packet carries only the changed pixels' box, and the
decorations at most every 5th tick while they changed. Player markers are
kept per holder, so each player's marker shows on a shared map, and go
when that player stops carrying it. A server's client now applies the
packet's decorations.
lan_e2e maps: host and guest each make and hold a map, which draws the
terrain around them and their marker, in both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's players (LAN guests, and single player's window on its server
thread) cast no lasting hook and caught nothing (#1348): the guest's cast
went into the game's own hook, which the server's tick then dropped for
want of a local player. Each guest now keeps its own hook (Player.fishing),
swapped in with the rest of its state for its turns, ticked as theirs every
server tick, and dropped when they die. Reeling in retrieves it on the
server: a catch, the rod's wear, the experience.
The server tracks every hook to its players as a fishing_bobber entity
(FishingHook.getAddEntityPacket: the owner's id; DATA_HOOKED_ENTITY and
DATA_BITING). A client takes the one its own player cast as its hook, so
the rod shows cast and the line runs to its hand, and draws the others'
bobbers on lines to their owners' hands.
A hook resting on a block stays on the ground (Entity.move keeps onGround
when nothing moves vertically); it used to flip every tick, so reeling it
in cost the rod 2 only half the time.
lan_e2e fishing: host and guest, both modes: the hook stays out and both
sides draw it, reeling in from the ground costs 2, and in open water with
Lure III a fish bites and is caught for 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify passed, with local proof: all 100 client harnesses ran on main and on the branch and their transcripts were diffed (89 byte-identical, the rest only timings or stamps). 81 client examples become one probe binary.
🤖 Agent: merged after verify passed, with local proof: all 217 world harnesses ran on main and on the branch and their transcripts were diffed (203 byte-identical, the rest only timings or known run-to-run counts). 168 world examples become one probe binary, with 1,097 duplicated helper lines and 11 dead examples gone.
A wind charge's burst (ServerExplosion with TRIGGER_BLOCK) pushed only the
game's own player, so a LAN guest, and single player's window on its server
thread, never rose (#1386). The burst now pushes every player in reach, each
swapped in as the player, and sends ClientboundExplodePacket to the players
within 64 blocks with the push it gave them (hitPlayers), which their client
adds (Entity.pushFromExplosion); onExplosionHit's fall-damage grace goes on
the server's body. A burst from a guest's own turn (their charge landing,
their mace's wind burst) waits for the turn to end, so that it reaches the
host and the other guests too.
The packet's centre particle is the gust emitter and it throws no block
particles, as WindCharge.explode asks; the client now draws the packet's
own particle. Explosion knockback resistance lowers the player's push as in
vanilla, and a guest's body knows whether it flies (no push while flying in
creative).
lan_e2e wind: host and guest each throw a charge at their feet, rise about
six blocks and land unhurt, in both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fully drawn bow fires a crit arrow: AbstractArrow.onHitEntity adds
random.nextInt(damage / 2 + 2) to the 6 damage, so one shot in five does 10
and kills the 10 health cow. The off hand bucket then had nothing to milk
("the host's off hand holds minecraft:bucket"), on either side, in any mode.
Under 20 ticks of draw the arrow is no crit and does at most 6. The check
now also asserts the cow is still there after the shot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st1: /particle and explosion particles from a server reach its players; lan_e2e evidence in the PR body).
BundleItem.broadcastChangesOnContainerMenu calls slotsChanged(player
inventory) after a bundle click. The menus that don't check which container
changed recompute from their inputs (crafting grids, loom, stonecutter,
cartography, merchant, smithing's hasRecipeError). The menu click harness
found it on a loom with a fixture result: 8000/8000 cases now match (seeds 7
and 11).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
🤖 Agent: merged after verify passed; main builds again. Local proof on the Mac mini: cargo check -p horizon-client --bins --tests fails on 8a4ebcc55 with the two E0599 errors and passes here.
#1432 deleted both as unused; #1424 (level events) and #1425 (scoreboard)
merged after it and call them, so horizon-client's binary failed with
E0599 at level_particles.rs:54 and scoreboard.rs:338. Both functions come
back unchanged from 4d7f30f14^1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every drawn character looks its glyph up in the font set's cache, and with
std's SipHash the hashing was the top cost of drawing sign text (#1461:
100 text signs in view). FxHash for those char keys cuts the frame's
prepare time with 100 text signs from 0.32 ms to 0.22 ms on the Mac mini
(Metal, 1280x720): 1920 to 2250 fps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shadow quads were lifted 0.001 above the block top and drawn in the entity
batch with depth writes. LivingEntityRenderer's translate(0, -1.501, 0)
puts a model's feet 0.001 above the ground too, so a chicken's feet and the
shadow under it were coplanar and flickered (#1440).
ShadowFeatureRenderer puts the quads on the block top itself
(relativeY + shapeBelow.minY), and RenderPipelines.ENTITY_SHADOW tests depth
without writing it. Shadows are now their own entity material (overlay 14)
drawn by such a pipeline after the entities. A small depth bias, constant
plus 1.5% of the slope, keeps them over the terrain under them, whose
vertices snap to the subpixel grid differently (without it whole shadows
vanished at some camera positions), while staying under feet 0.001 higher.
The shader pack shadow map no longer draws entity shadows as casters.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A carried bundle clicked on a stack took its place instead of taking it in
(#1456): Horizon's menus had no BundleItem click overrides. The PICKUP click
now runs them first, as AbstractContainerMenu.tryItemClickBehaviourOverride:
- a carried bundle takes as much of a clicked stack as its weight allows
(tryTransfer), and a right click on an empty slot puts its first stack
there (removeOne, the rest going back in);
- a stack clicked onto a bundle in a slot goes in as far as it fits, and a
right click with an empty hand takes the bundle's first stack out.
BundleContents.Mutable is in horizon_client::bundle (exact Fraction
weights: 1/max stack, 1 for a beehive with bees, a nested bundle's contents
plus 1/16; shulker boxes don't fit), used by the menu engine and the
LAN/server-thread container engine. The insert, insert-fail and remove-one
sounds play at the player.
In single player the inventory screen's empty-hand press went to an older
click path instead of the menu engine; it now goes to the engine, as on a
server, so the right click on a bundle works there too.
The menu click harness now has bundles (empty, loaded, nested; often in
hand) and compares the player's sounds: 3000/3000 bundle-heavy cases match,
2796/3000 without the overrides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreativeModeInventoryScreen.keyPressed on the search tab tries the hotbar
keys on the hovered slot before the search box: a digit or keypad key bound
to key.hotbar.N swaps when the cursor is over a slot holding an item, or any
slot that is not a display slot, and the cursor stack is empty
(checkHotbarKeyPressed). Over an empty display slot, outside every slot or
with a stack held, the key still types. Horizon sent every key to the search
box (#1460).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FirstPersonHandsAndItemsRenderer.submitArmWithItem draws a stack with a
map_id two-handed only from the main hand with an empty off hand; any other
map is renderOneHandedMap in its own hand. Horizon only had the two-handed
case and drew every other held map as its item model (#1459).
MapHold picks the case from the hand renderer's visible items, and
one_handed_map_pose is renderOneHandedMap's transform, checked against
vanilla's PoseStack. renderMap (background, map, decorations) is shared by
both cases. The arms under held maps are still not drawn, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Item.use runs on the client as well (MultiPlayerGameMode.useItem), and
only a use the client started sends RELEASE_USE_ITEM when the button
lets go. A server's client started one for bows and food only, so a
shield kept blocking after release, a charged trident never left the
hand, a loaded crossbow stayed "in use" on the server and never fired,
and a spyglass never scoped. The client now starts what the window's
own Item.use does: a bow or crossbow with ammunition, a trident, a
spyglass, a shield off cooldown, and keeps it until the button lets go.
The server's RELEASE_USE_ITEM let go of a main-hand bow only. It now
runs ServerPlayer.releaseUsingItem with the used item's hand, through
the release the window's own tick shares (bow, trident, spyglass).
A loaded crossbow reached its client without its ammunition: the
network stack gets minecraft:charged_projectiles (registry id 51,
ItemStackTemplate list), and a server's client decodes it.
lan_e2e gets a helduse check, for the host and the guest, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
🤖 Agent: merged after verify and CI parity passed (review-0-survey quick win: dead pub items; the generated version.rs is kept as the generator writes it).
A server's client never took the chest slot's Elytra into its own
movement (player.glider) and never told the server it started gliding,
so on the server thread and as a LAN guest the player just fell. The
client now sets the glider as the window's own tick does and sends
ServerboundPlayerCommandPacket START_FALL_FLYING when a glide starts.
The hub used to accept that command blindly and never clear it. The game
now answers it as handlePlayerCommand does (Player.tryToStartFallFlying,
else stopFallFlying), and each tick runs LivingEntity.updateFallFlying
for a gliding guest: the glide ends once canGlide fails (landed, riding,
flying, levitation, a glider about to break), every 20th tick wears the
Elytra, every 10th is an elytra_glide game event, and a glide that isn't
diving keeps the fall distance at 1 (checkFallDistanceAccumulation).
lan_e2e gets an elytra check, for the host and the guest, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's player (a LAN guest, or single player's window on its server
thread) could neither write in a book and quill nor read a written book:
the server opened the screens in the guest's turn and dropped them (#1366).
As in vanilla, the client opens a book and quill's editor itself when it
uses one (LocalPlayer.openItemGui) and sends ServerboundEditBookPacket
when it saves or signs; the server writes the pages or makes the written
book, signed with the player's name, in that hotbar slot or the off hand.
A written book opens on the client when the server sends
ClientboundOpenBookPacket for its hand (ServerPlayer.openItemGui).
horizon-protocol: OpenBook and EditBook (pages a list of at most 100).
horizon-ctl: book sign presses Sign, then Sign and Close; book done on a
book and quill saves it, as its Done button does.
lan_e2e books: host and guest each write, sign, read and drop a book,
both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sign a server's player placed (a LAN guest, or single player's window on
its server thread) opened no editor: its turn's editor was dropped with
the rest of the guest's screens (#1380). The server now does as
ServerPlayer.openTextEdit: the block, then ClientboundOpenSignEditorPacket,
and the player may write on that sign. The client opens its editor on the
sign it has and sends ServerboundSignUpdatePacket when it closes; the
server takes the lines (formatting codes stripped) unless the sign is
waxed, the player was not the one allowed or went out of reach, and sends
the sign to everyone. The window's own sign now reaches its guests too
(sendBlockUpdated, not only a save).
horizon-protocol: OpenSignEditor and SignUpdate, with SignTextSlot as a
VarInt and the lines as a fixed list of four (26.3's stream codecs).
horizon-ctl: type and key reach an open sign editor; block shows a sign's
lines.
lan_e2e signs: host and guest each place, write and see, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The guest death path (LAN guests, dedicated players and single player's
window on its server thread) gave the experience reward but never ran
Player.dropEquipment, so a player died with keep_inventory off and kept
everything (#1284). Both paths now share one drop_inventory:
destroyVanishingCursedItems, then Inventory.dropAll from the eyes less 0.3.
ServerPlayer.die skips the loot for spectators only; the window's own
death skipped it in creative too, which vanilla doesn't.
lan_e2e keepinv: host and guest, keep_inventory off and on, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's players (LAN guests, and single player's window on its server
thread) never had a totem ready, and nothing applied one: lethal damage
killed them with the totem still in hand (#1292). Each guest turn now
readies the totem from the guest's hands and, when its hurt set it off,
uses it up at the end of the turn (LivingEntity.checkTotemDeathProtection):
1 health, the item_used stat, the used_totem trigger, the use vibration,
the totem's effects.
Entity event 35 now goes to every client around, the holder's own too,
and a client plays it as ClientPacketListener.handleEntityEvent does: the
totem particles, the sound heard locally (not sent on as a sound packet,
so no client hears it twice) and the holder's rumble.
lan_e2e totem: host and guest, off hand and main hand, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shadow samplers bind with main's custom textures (texture.* first,
then the shadow map, then the rest); the shadow layers are drawn from the
shadow list on the flat path too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With main's Metal culling: the pack's depth test in OpenGL's layout runs
forced early on Vulkan (fs_box_early_depth) and in the shader on Metal
(fs_box_gl, the pack's depth attached read only and bound), and the flat
path culls too (phase 1 calls, flatten_phase2 after test_gl, then
draw_phase2 with each page's group 3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player's world ticks in the frame by default (#1428). When its ticks
take longer than 50 ms, every frame fell behind and ran the client timer's
ten, so the picture stalled at 1-2 FPS (a player's report on an AMD 680M,
in-frame tick at 36.7 ms). Vanilla's world ticks on the integrated server's
thread, which falls behind without costing frames and logs "Can't keep up!".
A frame now stops running world ticks once they took a tick's length and
drops the rest, logging vanilla's overload warning (1 s + 20 ticks behind,
at most every 10 s + 100 ticks). Ticks of a few milliseconds still get the
full ten per frame; sprints and server clients are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings main's shader api 2 (#1414) up the stack. One path for blending:
the terrain programs' blend.* now go through Pass.blend/buffer_blends like
composite and final (java_pack::pass_blends). depthtex1/depthtex2 are one
source, DepthSolid: Horizon's depth before translucent geometry, then the
pack's solid terrain's. Horizon's world goes to the pack's first terrain
target (colortex1 for MakeUp).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. The parity probe prebuild repeats each harness's own -p and features selection, so harness builds after the prebuild compile nothing (0 of 10, was 10 of 10). The full harness set ran on it locally in #1453's before/after diff.
🤖 Agent: merged after verify passed, with local parity proof (103/103 on this head). GUI sprites are measured in GUI units whatever the pack's resolution, as in vanilla: inventories, tooltips and toasts look right with Faithful 32x and 64x.
🤖 Agent: merged after verify passed, with local parity proof: parity --changed passed 103/103 on #1458's head, which contains this PR. Faithful 64x reproduced a startup panic on main (a 32768 px shared atlas over the 16384 GPU limit); with the fix it reaches the world. A pack that can't fit now falls back to the default resources, as vanilla does.
🤖 Agent: merged after verify and CI parity passed; release dry run 39935111815 was green (the smoke test drew a world, no blocker, it would publish as the release and latest). One channel, as Ludovic decided on 2026-10-23.
🤖 Agent: merged after verify and CI parity passed; lan_e2e (hands, styles, feedback, chat, frames, riding, mounts, beds, contacts, worldspawn) passes on the server thread and in-frame (bugs-st-2).
block_survival and state_provider hashed the name as an artifact instead of
passing it; decorated_region runs chunk_dump through chunk_generation's binary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. No runtime change for players: #1445 deletes 10 tools/ scripts that nothing references (no harness depends on them); #1446 exports the mod-SDK symbols only on wasm32, so the guest crates link on macOS, and the mod sandbox tests pass.
🤖 Agent: merged after verify passed. No runtime change for players: #1445 deletes 10 tools/ scripts that nothing references (no harness depends on them); #1446 exports the mod-SDK symbols only on wasm32, so the guest crates link on macOS, and the mod sandbox tests pass.
Behavior's Timed was copied into the villager, animal, monster and warden
brains: one copy now, in brain.rs, drawing its duration in Java's int
arithmetic (two copies used plain +, the same for every duration they
have). MoveToTargetSink's tryComputePath and reachedTarget, copied into
the villager, monster and warden brains on the same WalkTarget and Slot
types, are the villager brain's, given each brain's
CANT_REACH_WALK_TARGET_SINCE. NearestLivingEntitySensor's scan, copied
into the villager, animal and monster brains, is brain.rs's living_near.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed; docs only (AGENTS.md, CLAUDE.md symlink, docs/architecture.md). Every path and harness name was checked against the repo.
datafix: LeavesFix's getSideMask is ChunkPalettedStorageFix's, line for
line; chunk_leaves uses chunk_flatten's. mobs.rs: push_tests takes
sensor_scan_tests' 46-line cow MobType instead of its own copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
81 client examples that harnesses run become modules of examples/probe:
harnesses run `probe <name> ...` (or `cargo run --example probe -- <name>`),
and the shared helpers (audio_common, hud_fixture, chat_screen_fixture)
move with them. The benchmarks and manual tools stay standalone examples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main.rs now only lists the probes; dispatch.rs runs them. The runner leaves
main.rs out of a probe-binary harness's dependencies and keeps dispatch.rs,
so adding a probe no longer reruns (or flake-checks) every harness of its
crate. It also follows crate::<probe> references between probes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Eight benches (block_facts, chunk, entity_geometry, json_readers, load,
save, streaming, tick_chunk_membership), lan_flat, save_parity and
vanilla_diff: no harness, tool, doc or workflow names them (review plan,
section 1, dead code). -2,222 lines; Git history keeps them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Now that the probes are modules of one binary, the helpers copied between
them live once in examples/probe/support: the collision probes' shape
printer (emit, values, box_values, bounds, 8 copies each), the canonical
NaN bits of f64 and f32 (50 copies), the registry JSON-to-NBT reader (15)
and the JSON block position (4). -1,097 lines of copies.
f64::NAN.to_bits() is 0x7ff8000000000000, the literal some copies used,
so every probe prints the same bytes.
tools/parity.py follows include!: three probes include another probe's
source, and stay dependent on it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 168 horizon-world examples the parity harnesses run become modules of
one example, examples/probe, that dispatches on its first argument;
crate::args() gives each the command line its example saw. Linking ~170
binaries, each instantiating horizon-world's generics again, was most of
the parity job's probe build (review plan, section 1).
The harnesses build `--example probe` and run `probe <name> ...`.
tools/parity.py counts only the probes a harness ran, and the support
modules they name, as its dependencies, so editing one probe still reruns
only its own harnesses (and PARITY_RUST_SCOPE keeps working).
The six manual tools (benches, loot_fill, structure_blocks, vanilla_diff,
attribute_memory, behaviour_load_profile) stay separate examples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A record naming an example since renamed or deleted made the whole Cargo
run fail ("no example target named ..."), so no probe was prebuilt and
every harness built its own: the probe binaries rename ~250 of them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With Faithful 32x the inventory showed the top-left quarter of its
background stretched over the screen, tooltips lost their background and
toasts doubled in size (#1442): the GUI code drew every atlas sprite in
texels, where vanilla draws in GUI units whatever the pack's resolution.
- Atlas sprites carry their GUI size: the size of the default resources'
texture of that name (what vanilla's `blit(..., 256, 256)` calls and
sprite sizes assume), their texels only for what copies pixels (font
sheets, fire frames). Textures only a pack has keep their own size.
The texture size filter reads the GUI size too, so Faithful 64x's
1024x1024 container backgrounds are no longer dropped.
- Nine-slice sprites follow `GuiGraphicsExtractor.blitNineSlicedSprite`:
their `gui.scaling` width, height and borders (Faithful's tooltip
background is 50x50 with a 10-pixel border), `stretch_inner`, the
single-quad and three-part cases, and borders capped at half the
drawn size by integer division.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shared GUI atlas was always 4096 texels wide, so Faithful 64x needed
4096x32768: past the 16384 texels a side GPUs allow, and wgpu panicked
in create_texture at every launch until the pack was removed (#1442).
- The atlas widens (4096, 8192, 16384) until it fits within 16384 a
side, so Faithful 64x loads. Its height rounds up to a multiple of
1024 instead of a power of two: the default resources' atlas drops
from 4096x8192 to 4096x5120, Faithful 32x's from 4096x16384 to
4096x12288.
- Textures that still don't fit are an error, not a GPU panic. At
startup it rolls the packs back as Minecraft.rollbackResourcePacks
does: the game starts with its own resources, and the selection is
cleared and saved (clearResourcePacksOnError), so the next launch
doesn't fail again. The existing "Resource reload failed" screen
shows the reason.
- The renderer refuses an atlas larger than the device allows with an
error instead of a wgpu validation panic.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed; release dry run 39935111815 was green (the smoke test drew a world, no blocker, it would publish as the release and latest). One channel, as Ludovic decided on 2026-10-23.
AGENTS.md (CLAUDE.md links to it): the crates and what each owns, how to
build and test fast, where each subsystem of horizon-client lives, how Game
is organised, the two single-player paths and how to test both, the vanilla
reference, and the rules that bite. docs/architecture.md: feature -> files ->
checks for the 40 areas changed most in the last 30 days.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed. Metal GPU occlusion culling: on the M4 Pro at RD 32, FPS is +8% to +48% (interleaved A/B against main); the hidden sections match Vulkan's; parity --changed passed 104/104.
The 120-line hand-written central-directory and local-header parser goes:
the zip crate (already the client's dependency for world backups) indexes
the archive, zip64 and odd offsets included. Entries are still read raw and
inflated here, so as with Java's ZipFile (vanilla's FilePackResources)
their CRC isn't checked. New tests: folder entries, a nested pack root, a
Windows-style name, one broken entry among good ones, and files that aren't
zips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
free_port (7 copies), offline_uuid (4), varint and parse_varint (3 and 2),
dbits (6), fbits (4) and settled (2) move into lab.py; every caller uses
lab's. The other server harnesses keep importing free_port from
test_dedicated_server_parity, which now takes lab's. Imports the moved
code left unused go too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-mods' runtime is no longer a default feature, and its sandbox test
requires runtime and pack instead of a dev-dependency on itself (that
dev-dependency turned the runtime on in every workspace build with examples,
the parity probe build included). The client gets a mods feature that turns
the runtime on; without it game/mods.rs keeps content mods and logs why code
mods don't start, and every hook does nothing.
Release builds (client and server) pass the feature, CI's clippy and tests
pass it (and pack, for the sandbox test), and pr_ready does where horizon-mods
is checked. horizon_mods::find moves to manifest.rs: content mods use it
without the runtime.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The file is generated from the pinned version lock, and test_gen_version_consts
checks JAVA_VERSION against it; deleting constants by hand breaks the next
regeneration and that test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A host build of a mod (cargo build or test on the workspace) made a cdylib
whose component exports macOS's ld couldn't take: its exported-symbols list
cuts `horizon:mods/[email protected]#init` at the `#`, so coords-hud and
horizon-mods-test-guest failed to link. The exports now exist only for
wasm32; on the host the macro still checks the mod and keeps it used.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after the contribution gate. The claims were checked against Iris and OptiFine, and the weak parts were rewritten on the branch: the stage kinds, the Java shadow feed and the fake uniforms were removed; settings now apply on Done; packs are imported by drag and drop; memory is only allocated while a pack reads it; the MakeUp target is fixed. Tested on Vulkan and Metal with 6 packs, profiles, imports and broken packs; local parity passed 96/96. Frame times before and after are in the PR comment. Thanks @kerneldroid.
command_parse_summary, structure_start_oracle, fetch_client_assets,
compare_structure_starts, profile_behaviour_cache, command_tree_diff,
compare_loot_fill, compare_structure_blocks, test_lan_lifecycle and
PROFILE_SHARED_JSON.md: no workflow, doc, harness, test or other tool
names them (review-plan.md, Dead code). History keeps them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
world_spawn starts a vanilla and a Horizon server for each of 60 seeds,
one at a time: 1,144-1,548 s on the CI runner, the parity job's longest
harness and its floor whenever a PR's changes reach it. With
PARITY_SHARDS = 4 each shard creates every fourth seed's worlds, in its
own build directory.
The shard helper datafix had moves to tools/parity_shard.py, which both
use. It stays out of lab.py, which every harness imports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
As in Iris's ShaderPackScreen: at the end of the list it scrolled out
of sight with a few packs. It's greyed out while the picked pack has no
options.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The probe prebuild ran `cargo build --workspace --examples --bins` (or
named targets with --workspace). The harnesses build with `-p <crate>`, and
Cargo unifies features over the selected packages: under --workspace,
flate2, libc, memchr, syn, thiserror, postcard and digest get other
features for horizon-world, and horizon-mods (pack) and horizon-telemetry
(upload) for horizon-client. So every probe the prebuild made was a
variant no harness used, and each harness rebuilt its crate and its probe
again while the others waited on Cargo's lock.
Record each probe with its command's package and feature flags and
whether that command builds examples (which turns dev-dependencies on),
and prebuild one Cargo run per such selection. --bins and --examples are
recorded too, so the fast-profile horizon-client build that
save_roundtrip and world_reopen run is prebuilt as well. A harness that
never ran here takes its probes from the shared dependency lists instead
of a build of all ~275 examples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
cargo test --workspace built and linked all ~275 examples, 59% of the
test build (review-plan section 1), though none of their tests run.
Test the libraries, binaries and integration tests, then the doc tests,
as tools/gate.py and tools/pr_ready.py already do. Clippy --all-targets
still type-checks every example, and the parity job builds and runs them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world target made the opaque depth, normal and material textures
for every scaled or anti-aliased frame, packs or not: 12 bytes a pixel
nobody read. They now follow what the pack reads, and Post::generation
tells the stages to bind again when its targets change (a recreated
target of the same size, or a shadow map made after the stages bound).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MakeUp's gbuffers write colortex1, and its composites read the world
from there: with the world in colortex0, it showed nothing but fog.
Iris's fallbacks for terrain pick the program (gbuffers_terrain, then
textured_lit, textured, basic); its first draw buffer takes the world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review fixes for #1414 (contribution gate):
- Java packs no longer get Horizon's shadow map: it was mirrored in y and
reversed in depth against the GL conventions the packs sample with, had
no distortion and only the camera's sections, so BSL drew no shadows
while the pass cost 0.65 ms (MakeUp, M4 Pro). shadowtex* stays a far
depth, as on main; the packs' own shadow programs are #1326.
- Invented uniforms removed (centerDepthSmooth fixed at 0.5,
ambientOcclusionLevel and shadowMapResolution as uniforms, constant
temp/rainfall/biome/held items); an unknown uniform stays zero as
before. wetness keeps its Iris smoothing. Unused directives removed.
- texture.<stage>.<sampler> is scoped to Iris's stages (final reads the
composite ones) instead of overriding the sampler in every pass;
texture.noise replaces noisetex. One noise generator and one RGBA
upload for both pack kinds.
- The G-buffer pass, the opaque-depth copy and the shadow pass run only
for a pack that reads them (naga's per-entry-point binding use, not a
text search): every pack paid all three before.
- Api 2's prelude additions go in their own file after api 1's, so api 1
packs keep the names they declare (aces_tonemap, material_at...).
- Stage kinds shadow, gbuffer, deferred and final ran as composite: only
composite is accepted; api 2 adds formats (validated), profiles,
option descriptions and a noise texture.
- Shader Pack Settings applies once when the page closes, as Iris's Done
(each click or slider step rebuilt the pack: 2.7 s per click, 12.7 s
for one drag with BSL). Labels follow Iris (.lang, else the name).
A pack whose options can't be read isn't read again every frame.
- Saved option values an option doesn't list are ignored, as OptiFine's
ShaderOption.setValue (a stale file broke BSL's compile).
- Import: the blocking file dialog (osascript/zenity/kdialog/PowerShell
on the event loop) is gone; a pack dropped on the Shader Packs page is
copied and picked, as Iris's onFilesDrop. Drops elsewhere do nothing,
an existing pack isn't overwritten, links aren't followed.
- Docs and the example say what the code does (SSAO, not GTAO).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Apple's GPUs the passes the test adds cost a fixed share of the frame
(it is stored and reloaded at each), which the hidden sections repay only
at high render distances: at 12 culling cost an M4 Pro 0.05 ms of GPU time
a frame, at 32 it saved 0.25 to 1.6 ms. Other backends keep culling every
frame; HORIZON_GPU_CULL=all culls every frame on Metal too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed; lan_e2e (hands, styles, feedback, chat, frames, riding, mounts, beds, contacts, worldspawn) passes on the server thread and in-frame with all four merged on main (bugs-st-2).
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
horizon-client's dev-dependency on naga added the hlsl-out feature for the
gfx.rs test that translates every shader for DirectX 12. With resolver 2 a
dev-dependency's features apply only to tests and examples, so Cargo kept
two builds of naga, wgpu-hal, wgpu-core, wgpu, horizon-shaderpack and the
horizon-client lib, and rebuilt them on the first example or test after a
game build.
Measured on the Mac mini (cargo build of the game, then one example):
the example took 37.3 s and recompiled that whole stack before, 1.2 s
and only its own crate after; the target dir is 1 GB smaller (4.9 to
3.9 GB). The game binary is the same size: nothing calls the HLSL writer,
so the linker drops it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed. In-frame single player is the default again until the server thread reaches parity (FEATURES 4zg); release builds were compared with v0.2.0 and v0.2.11 on the Mac mini. The server thread stays available with --server-thread or HORIZON_SERVER_THREAD=1.
93 pub items (78 functions and methods, 14 constants, 1 struct) in horizon-client,
horizon-world, horizon-core, horizon-friends and horizon-server-files had
no caller anywhere in the repository: not in the crate, the other crates,
the examples, the tests or the binaries. rustc can't flag them because
horizon-client and the others are libraries, where a pub item counts as
used. The imports and the private helpers only they used go with them
(title.rs date_string, goals.rs flag_index).
Also drops the #[allow(dead_code)]/#[allow(unused)] items that only hid
dead code: chat_debug_fill, merchant_title and Goals::locked_by had no
caller, and four stubs (_keep, _unused, _rule_kind, const _) existed only
to keep an import used. LightTables::from_parts is used by its tests, so
it is #[cfg(test)] instead of allowed.
Protocol id tables and entity data serializer ids stay: they mirror
vanilla's registries in full. The launcher's three unused items are left
for a launcher PR.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Flat draws (one index list per frame, the default on Apple GPUs at high
render distances) turned occlusion culling off. Now phase 2's draws are
flattened after the test, by a compute pass that copies only the sections
the test saw, and each page run is drawn indirectly with the count it
reached.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GPU occlusion culling ran on Vulkan only: its test needs early fragment
tests, which naga's Metal backend does not emit. On Metal the boxes are
tested in a pass of their own: phase 1's depth is bound read-only as the
attachment and as a texture, and the fragment shader compares its own
depth with it (reversed Z, greater or equal), as the fixed-function test
would. HORIZON_GPU_CULL=shader takes this path on Vulkan too, for checks.
Frames drawn flat (the default on Apple GPUs at high render distances)
still skip culling here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every release is now published as the release (one channel, Ludovic's
decision of 2026-10-23), so the launcher drops its Stable/Alpha choice:
the Updates row in Settings, `horizon-launcher update-channel` and its MCP
tool, the channel on the Versions page, in `versions` and in the footer.
It updates to the newest game release that is not a pre-release (GitHub's
latest) and looks every 30 minutes. The pre-releases of the old Alpha
channel stay listed and installable by hand, never as an update. Old
`update_channel` / `beta_builds` keys in settings.json are ignored.
On Linux a release build that starts no longer rewrites the
applications-menu entry to point at itself. It writes the entry only when
there is none, or when the program it starts (TryExec, else Exec) no longer
exists; turning the Settings switch on or `desktop-entry` still points it
at this launcher, and HORIZON_LAUNCHER_DESKTOP_EXEC is still honored. A
test build started with the real HOME took over a working entry and then
left it broken when its target/ was deleted (2026-10-23).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic's decision (2026-10-23): no more Alpha and Stable. release.yml
publishes every game release as the release and GitHub's latest instead of
a pre-release that promote.yml made stable once a day. The checks that
gated a promotion run in the release itself: `decide` holds the train while
an issue labelled release-blocker is open (forced runs too), and the publish
job runs `release_train.py gate` right before publishing: the Mac mini's
smoke-test.json must be there and show a world was drawn, and no blocker may
be open. Otherwise nothing is published.
Notes cover everything since the last release that is not a pre-release, so
the first single-channel release lists all of it since v0.2.0. promote.yml,
tools/release_channels.py and its tests are deleted. The cadence is
unchanged (5 minutes of settling, 20 at most, release-now at once).
Docs and site: one channel in release-flow, versioning, security, the FAQ,
install, the home page and the Releases page (Game / Launcher filter); no
player-facing Alpha wording left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
Players on the Alpha channel report single player as broken since v0.2.1
(#1265 made the integrated server thread the default again). On v0.2.11's
server thread, placed signs open no editor (#1380), held books don't open
(#1366), a totem doesn't save its holder (#1292) and death keeps the
inventory with keep_inventory off (#1284); the in-frame world and Stable
v0.2.0 do all four right. About 45 more [server-thread] issues are open.
`--server-thread` or HORIZON_SERVER_THREAD=1 turns it on, as before #1114;
`--no-server-thread` and HORIZON_SERVER_THREAD=0 still work. LAN guests and
dedicated servers are untouched. Temporary until the server thread reaches
parity (FEATURES 4zg).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server never sent particles: /particle on single player's server thread
drew into the server's own engine, and TNT exploded without its burst for
anyone but an old-mode host. The protocol gains
ClientboundLevelParticlesPacket and ClientboundExplodePacket; /particle
sends the particles to each viewer in range (every player without
viewers, as PlayerList.getPlayers) and an explosion sends its packet to
the players within 64 blocks; their clients draw them as
handleParticleEvent and handleExplosion do.
/particle's spread was measured from the command's source as if it were a
position; it is the spread as given (Vec3Argument.getVec3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server never sent its scoreboard, so a sidebar shown on single
player's server thread or to a LAN guest stayed empty. Each tick a placed
player is now sent what changed since it was last sent the board, as
ServerScoreboard's callbacks do: every team, and the objectives in a
display slot with their slots and scores (all of it on joining).
A server's client also finds its team's sidebar by its profile name rather
than single player's "Player".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) played only three of the level events the server
sent (a block broken, the dragon egg's and a consumable's teleport): a
spawner's burst, waxing and scraping copper, a composter filling and the rest
showed nothing. Every one now goes through the handler the in-frame game uses
(LevelEventHandler).
Bone meal's level event (1505) played its sound but never its growth
particles, in either mode: it now spawns them as BoneMealItem.
addGrowthParticles does (spread over a grass block's, nylium's, netherrack's
or moss's neighbourhood and water, inside a growing block, below rooted dirt
and mangrove leaves). The server also sends it to the player who used the
bone meal, as vanilla's levelEvent names no player.
Waxing copper with honeycomb and an axe or hoe's transforms (wax off, scrape,
strip, till) make their particles and sound on the clicker's own client, as
vanilla's client runs Item.useOn; the server sends them to the others only.
The transform's preview is shared with the in-frame use.
`horizon-ctl status` counts the particles made by type. lan_e2e's new
`sparks` check: the host and the guest each use bone meal on wheat, wax
copper and take the wax off, and a spawner spawns a zombie; both sides make
the particles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A splash or lingering potion, an experience bottle or an eye of ender a
player threw was never an entity a server tracked: on single player's server
thread, for LAN guests and on a dedicated server the item was used up and
nothing flew.
The server now numbers them like its other projectiles and tracks them as
vanilla's entities: splash_potion, lingering_potion and experience_bottle
with ThrowableItemProjectile.DATA_ITEM_STACK (the potion, for its colour),
and eye_of_ender. A client draws them where the server puts them and drops
them when the server removes them.
lan_e2e's new `thrown` check: the host and the guest each throw a splash
potion and an eye of ender, and both sides see them fly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server never told its clients who holds a mob's lead, so on single
player's server thread, a LAN guest's or a dedicated server's client the
lead was used up and the mob tied, but no rope showed.
The tracker now sends ClientboundSetEntityLinkPacket as vanilla does: in
ServerEntity.sendPairingData for a leashed entity, and when the lead is tied
or comes off (Leashable.setLeashedTo / dropLeash, the holder 0). The holder
is the player the mob sees (a guest, else the host) or a mob; a fence knot
isn't an entity on Horizon's server yet. The client sets the mob's holder
from it: itself, another player or a mob.
lan_e2e's new `leads` check ties and unties a cow on both sides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) only applied a rabbit's baby flag and a creeper's
swell from SetEntityData. It now reads the synched data vanilla's entities
read in onSyncedDataUpdated: Entity's on-fire flag, LivingEntity's health
(the wither's boss bar follows it), the baby flag of every ageable mob,
Sheep.DATA_WOOL_ID, TamableAnimal's sitting and tame flags, the wolf's and
the cat's collar colour, and the cube mobs' size. A remote wolf or cat starts
with vanilla's red collar.
The server sends the wolf's and cat's collar colour. Pairing still carries
only the values that differ from the defaults; a mob's state now lists the
defaults of the entries that can go back to theirs (health, the baby flag,
the wool, the collar), and the tracker sends that default when one does:
a calf growing up or a sheep's white wool growing back reach the clients.
Sheep saves wrote the colour and shearing they were loaded with (0 unless
the save had them) and loads ignored both: they now save and load the wool
as it is (Sheep.add/readAdditionalSaveData).
lan_e2e's new `looks` check summons the mobs and changes them on both sides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server told joining players of a default border and never of a
change, so on single player's server thread and for LAN guests the wall
neither showed nor stopped anyone. Players now get the real border on
entering a level and each change as BorderChangeListener sends it.
The player's movement collides with the border's walls
(CollisionGetter.borderCollision) instead of being clamped after moving,
which a sprint-jump got past. A guest (and single player's own player on
its server thread) outside the border takes its damage on the server, as
the host's player did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client of a server (single player on its server thread, LAN guests)
ignored ClientboundUpdateAttributesPacket for its own player, so a new
maximum health never reached it: its hearts stayed at ten and the health it
was sent was clamped to 20. It now applies each synced instance's base and
modifiers (handleUpdateAttributes) and re-applies the received health.
BossBar::new started hidden; ServerBossEvent starts visible.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
save_roundtrip found an allay saved with InLove 0: Horizon's `animal`
flag is broader than vanilla's Animal class.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed; #1190 has interleaved GPU A/B against main on the HD 620 and M4 (within noise) and 103 parity harnesses passing; #1413 was proven with a 1080p run on the desktop.
🤖 Agent: merged after verify passed; #1190 has interleaved GPU A/B against main on the HD 620 and M4 (within noise) and 103 parity harnesses passing; #1413 was proven with a 1080p run on the desktop.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
- Critical hits also need the attacker out of water, not blind and not
riding (Player.canCriticalAttack) (#1393).
- Mending repairs the off hand too: candidates in EquipmentSlot order,
main hand, off hand, then the armour (#1349).
- Phantoms spawn only in the Overworld with spawn_mobs, spawn_monsters and
spawn_phantoms on, near every player (the local one and each guest in
the level, with their own time since rest), with vanilla's draw order
(#1388, #1387).
- A level entered by a portal or a dimension change keeps the world's
natural_health_regeneration (#1339).
- /item replace ... with air empties the slot, and counts it as changed
as vanilla's SlotAccess.set does (#1332).
- Thrown potions stay where they are on frozen ticks (/tick freeze) and
land once the game runs again (#1364).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A connection's chat keeps the server's component: SystemChat goes
through the same translation and styles as the action bar, and player
and disguised chat are decorated from their chat type's translation,
parameters and style (ChatType.Bound.decorate), so colours, bold,
hex colours, and the client's language are kept (#1372, #1389).
- Components parse `click_event` (suggest_command, copy_to_clipboard; never
open_file from a server) and `hover_event` show_text (#1377), and the
chat screen draws the hovered part's tooltip (#1381).
- `§` codes in chat colour the text with Chat Colors on and are stripped
with it off (ComponentRenderUtils.wrapComponents) (#1379).
- A command's operator broadcast skips the player whose command it was on
the integrated server too (#1373), and /give fires InventoryChangeTrigger
before its message, whose announcement is sent first (#1375).
- The control socket's `messages` gives the chat lines as components;
lan_e2e `styles` checks a styled tellraw, the advancement order and the
missing echo for the host and the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The remote client sends Interact for a targeted mob only for a hand
vanilla's client-side Mob.interact would accept with it (leads, name
tags, food, buckets, shears, saddled mounts, tamed pets, ...), so a bow
drawn or food eaten while aiming at a cow goes on to useItem (#1350).
- Both hands: the main hand's item, then the off hand's entity, block and
item (Minecraft.startUseItem). The server runs Interact with the packet's
hand (GuestUse::Entity), and mobs, armour stands, leads and mounts use
the held hand's stack, so an off-hand bucket milks a cow and off-hand
wheat feeds it (#1362).
- The entity tracker sends an item frame's item and rotation when they go
back to empty and 0, so a punched frame shows empty on clients (#1355).
- lan_e2e `hands`: the bow, the golden apple, the off-hand bucket and wheat
and the emptied frame, for the host and the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Guests' player data saves Fire, Air and fall_distance and puts them back
on their body when they come back (single player's players are guests
of the integrated server), so a burning player still burns after a
reopen (#1323); the old single player's level.dat keeps fall_distance
too (#1327).
- Every Animal saves and loads InLove, so a fed animal stays in love
across a reopen and is not fed again (#1396).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The sneaking case read the health after releasing Shift and waiting 10
ticks on the magma, so a hit from standing up counted (and one could land
between the first reading and the key press).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bed sets the respawn point as vanilla's ServerPlayer.startSleepInBed does
(once in reach, day or night): a RespawnConfig at the bed's head with the
player's yaw, not forced; straw beds never set it (straw_bed_rule).
Respawning resolves it through findRespawnAndUseSpawnBlock, which now
handles beds: AbstractBedBlock.findStandUpPosition (beside the bed on the
side away from the yaw it was set from, around it, then on it; bunk beds
too) through DismountHelper, facing the bed. A bed that is gone or boxed
in sends the player to the world spawn with "You have no home bed or
charged respawn anchor, or it was obstructed", and the point is forgotten:
in the window's world, on the server thread and for LAN guests. Leaving
the End goes to the bed too (not using it up).
Saves keep a bed as the player's `respawn` (resolved through its block
when loaded), and a `spawnpoint` in the Overworld as forced. A fallback
that is the world spawn follows `setworldspawn` with a bed or anchor set.
tools/test_use_item_on_parity.py: respawn cases at a bed's head set from
some yaw (intact, bunk, only the foot left or gone, crowded), against
vanilla's real findRespawnAndUseSpawnBlock; 4000/4000 and 12000/12000 on
two seeds. lan_e2e `beds`: intact, gone and boxed in, host and guest.
Fixes#1308
Co-Authored-By: Claude Opus 5.5 <[email protected]>
perf_bench ran every game in a 1280x720 window only. It now runs the
standard set at 1280x720 and 1920x1080 (--sizes picks others), each size
its own reports: <label>-rd<N>.json at 720p as before, <label>-1080p-rd<N>.json
at 1080p.
Horizon takes vanilla's --width and --height for its window's starting size
(1280x720 by default); perf_bench passes them to every game, and to Horizon
only off 720p, so builds before this one still run the 720p set.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The vignette, drawn over every frame, went from one texture sample to up to
four texel loads and per-pixel address arithmetic: +0.85 ms of GPU a frame on
an HD 620 (integer modulo, removed in the commit before), still +0.4 ms after
it.
- A blurred sprite gets a one-texel border in the atlas, copied from its
opposite edges: the hardware's bilinear sample of the atlas with the
entity group's linear sampler is then GL_LINEAR with repeat, edges
included. Clamp-to-edge keeps the sample point half a texel inside the
sprite, which is what the hardware's clamp does for both filters.
- Each sampling has its own pipeline, the shader specialised by override
constants: a repeat sprite runs main's arithmetic and one sample, nearest
or linear; only clamp adds a clamp. The sampling moves from the vertices
to the quad, so the vertex layout is main's again.
- A black vignette changes no pixel (it darkens by texture x colour), and is
no longer drawn: in daylight its brightness fades to 0 within half a
minute, so most frames outdoors skip it, which main never could.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and a proof run on the Mac mini: the self-check passed (parity 22/22), and six TNT minecarts now chain (6 → 0; 5 survive on main). Thanks @levzzz5154!
🤖 Agent: merged after verify passed and a proof run on the Mac mini: the self-check passed (parity 22/22), and lan_e2e minecarts passes in both single-player modes. Thanks @levzzz5154!
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
The server sends a knocked-back, pushed or jumping mob's move at once
(Entity.needsSync), and the remote client walks every living entity along
the server's positions with vanilla's SteppedInterpolationHandler: each
position takes the type's update interval to reach, sped up while steps
pile up. Before, the client waited for the next due update and then
lerped there in fewer ticks, so a hit pig jumped away fast.
tools/test_stepped_interpolation_parity.py drives vanilla's unmodified
InterpolationData and the Rust port with the same packets and ticks
(random cases and a punched pig's knockback); 135236 positions and
rotations match bit for bit.
Fixes#1374
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemEntity.tick: an item stuck inside a full block (one set on it) has no
physics on the server and moves toward the closest free side
(Entity.moveTowardsClosestSpace: north, south, west, east, up; 0.1 to 0.3 a
tick), instead of staying in the block (#1252).
A server's client (LAN guests, and the window on the server thread) digs at
the same speed as the server: getDestroyProgress with the submerged mining
speed, haste, fatigue and efficiency, not the plain tool speed, so it no
longer finishes early under water and gets the block put back (#1254).
lan_e2e: `drops` (a diamond under a block set on it comes out, in both
views) and `underwater` (dirt by hand at eye height under water: about
halfway at 35 ticks on client and server, broken by 105).
Fixes#1252, #1254.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SetWorldSpawnCommand sets the level's spawn and its angle; a player without
a bed or anchor respawns there (it used to set the player's own spawn
point, so other players and later deaths kept the old one). The angle is
saved in level.dat's spawn (yaw, pitch), and a guest whose saved respawn
point is the world spawn follows it when it moves.
lan_e2e `worldspawn`: /setworldspawn 24 blocks east, then the host and the
guest die and respawn there.
Fixes#1310.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemCooldowns ticks once a tick for each player: a server's players count
their shield cooldown down in tick_guests, and the host's world in tick_world
(it was in tick_using, which ran for the host and again for every guest's
swap, so a guest's shield came back early on the server thread).
A guest that dies stops digging (its mining, the delay and the crack go), so
the block it was breaking doesn't break once it respawns.
lan_e2e: `shield` (a vindicator's axe disables each player's shield; the
server's cooldown then falls 40 in 40 ticks) and `deathmining` (die while
breaking obsidian, respawn, the obsidian stays). `lan status` and the agent
state report the shield cooldown.
Fixes#1315, #1316.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.applyEffectsFromBlocks: MagmaBlock.stepOn burns a player standing on
it who is not sneaking (hot_floor, 1), CactusBlock and CampfireBlock.entityInside
hurt what touches them (cactus 1, campfire 1, soul campfire 2, lit only).
A server's players (LAN guests, and the host on the server thread) now get
these and the grown sweet berry bush prick along the moves their client sent
that tick: the server rebuilds the tick's movement history and runs the
damage half of the block effects.
lan_e2e `contacts`: magma, sneaking on magma, a lit campfire, a cactus, grown
and young berry bushes, for the host and the guest.
Fixes#1305, #1311, #1312, #1313.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server sends a player's own air supply and absorption amount
(Entity.DATA_AIR_SUPPLY_ID, Player.DATA_PLAYER_ABSORPTION_ID) when they
change, and the client applies them: on the server thread and as a LAN
guest the breath meter follows the server's Respiration-extended air
(#1304) and the golden hearts show (#1306). The connected client's tick
sets the helmet's Respiration and the Nether's FAST_LAVA as the window's
own tick does (#1309).
AbsorptionAmount is written from the live body and read back on load,
in the window's world and for a server's players (#1307).
lan_e2e: absorption, breath and lava checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The camera overlays (the vignette every frame) read up to four texels a
pixel, each wrapped with two integer modulos. Intel GPUs emulate integer
division. Reducing the coordinate to one period first (fract, as the shader
did before) leaves every read at most one texel outside the sprite, so a
select wraps it. Same texels and weights; clamp-to-edge is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
On Apple GPUs timestamps go only at pass boundaries, and the frame was
bracketed by two one-thread compute passes. Two things made it read
about 0.01 ms:
- Metal resolves counter samples without waiting for the passes that
write them. Resolved in the frame's own command buffer, the end
timestamp was mostly the one an earlier frame left in the shared
query. Each readback slot now has its own queries, and with markers a
slot is resolved in a later frame, once on_submitted_work_done says the
GPU finished the frame it timed.
- A compute pass depends on nothing, so the end marker ran alongside the
frame's render passes. The end is now a render pass that loads the
frame's target, shading one pixel with every write masked: it runs
after every pass that drew into the target. The start stays a compute
pass (cheaper than a load and store of the target).
M4 Pro, render distance 32, offscreen: GPU p50 0.002 ms before, now
1.95 ms against a 1.95 ms frame (village) and 4.46 ms against 4.51 ms
(mountain), where the frame is GPU-bound. Vulkan and DirectX 12, which
write timestamps between commands, are unchanged but for the per-slot
queries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
website/docs/agents.md, in the nav under "Going further", is the player's page.
It covers what an agent can do, connecting it from the launcher, copy-paste
config for Claude Code, Codex, Cursor and any MCP client, the game's AI Agent
page, a CLI quick reference, safety and No AI.
docs/agent-control.md is rewritten for developers:
- the surfaces, and why one MCP server covers the launcher and the game;
- the inventory, before and after, for the launcher and the game;
- No AI on every surface, and the launcher commands that stay;
- connecting from the launcher and the game;
- the design rules for a new command.
docs/launcher-commands.md lists every launcher command, its arguments and its
MCP tool. It is generated from the clap definition (cli_tools::markdown). The
launcher test the_launcher_reference_is_up_to_date fails when it is stale or a
tool is undocumented, like horizon-ctl's generated reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon Settings gains a row: "AI Agent..." and "No AI: Off". No AI hides the
AI Agent page and closes the control socket at once.
The AI Agent page has an Agent Control switch (horizon-extras.txt
agentControl, off by default): on, the game the player starts opens the
default control endpoint, so horizon-ctl and MCP clients reach it without
--control. Turning it off closes the endpoint at once; dropping a control
server now also stops its listener thread. The page shows whether an agent
is connected, when it last sent a command and the socket, and Copy
Connection Details copies the socket path, a horizon-ctl line to try and the
MCP config for this game.
agent_control_e2e.py --scenario setting covers it: Agent Control on opens the
endpoint without --control, the pages list their widgets, Copy works, and
turning the switch off removes the endpoint while the game keeps running.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main (#1271) pins Horizon Settings and Mods as the grid's last buttons;
this branch adds Credits & Attribution after them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/lan_e2e.py: main added the mounts check; levels stays last (it moves
the players between dimensions, and skips itself without --server-thread).
CHECKS and CHECK_FNS: 67 each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#1271 removed the title's live world backdrop (backdrop_live), so the
locator bar hides on the title alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/lan_e2e.py: main added the minecarts, frames, lightning and riding
checks; levels stays last (it moves the players between dimensions, and
skips itself without --server-thread). CHECKS and CHECK_FNS: 66 each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- game.rs: main (#1271) dropped the Horizon-theme checks from menu_blur and
the title scroll; kept without them, with this branch's WinScreen parts
(the credits alone blur, the title doesn't scroll under the Win screen).
Screen keeps main's Friends and this branch's Win.
- control.rs: horizon-ctl options pages keep credits, horizon and
shader-packs.
- options_ui.rs: Page keeps Credits and main's Horizon. The Options grid is
main's (Horizon Settings and Mods in vanilla's Telemetry and Credits slots;
Horizon Extras now sits inside Horizon Settings) with Credits & Attribution
after them, laid out by this branch's row count.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
hud.rs: HudAtlas::slots keeps this branch's trim size parameter, with
main's doc line (the on-screen keyboard's text replaced the Horizon menu
text, #1271).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
graphics.rs: main (#1271) dropped the level key from horizon-graphics.txt;
farDetail stays after lodDistance in main's format (21 keys, 21 values).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A section's shadow is its box swept away from the light; when the box
is outside one of the camera frustum's planes and the sweep never
crosses it, nothing it casts can be seen (Iris's advanced shadow
culling, conservatively). The picture is the same; BSL's 256-block
shadow distance draws far fewer sections.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerExplosion.hurtEntities skipped minecarts: a TNT minecart's blast
never reached the carts beside it. Carts now take the explosion's damage
through VehicleEntity.hurtServer (destroyed past 40, where
MinecartTNT.destroy primes a TNT cart with a 0-39 tick fuse, as
damageSourceIgnitesTnt counts explosions) and the knockback from their
eyes. The exploding cart is the explosion's source, so it is removed
before the blast and can't hit itself. Cart ticks now look carts up by id,
since a blast can remove or prime other carts mid-loop.
OldMinecartBehavior.tick ends with pushAndPickupEntities: after a cart
moves, every other cart in its box (0.2 wider) gets push(cart). Horizon
pushed all pairs once before any cart moved, so carts placed inside each
other stayed exactly overlapping (push skips distances under 1e-4) and
never split apart. Pushes now run per cart, after its move, with
pushOtherMinecart's Mth sin table facing and float thresholds.
A Java pack's shadow program (shadow_cutout, shadow or shadow_solid for
the solid layer, shadow_water or shadow for the translucent one) draws
Horizon's terrain from the light after begin, as Iris's ShadowRenderer:
- shadowModelView and shadowProjection are Iris's ShadowMatrices, with
the pack's shadowMapResolution, shadowDistance, near and far planes,
shadowIntervalSize and sunPathRotation; shadow programs get them as
their model view and projection.
- The renderer picks every resident section inside the light's box (no
cave culling, no face culling) and draws it instanced, solid then
translucent, with origins after the camera's.
- shadowtex0 holds everything, shadowtex1 the depth before translucent
terrain, bound as depth textures for comparisons and as float copies
for plain reads; shadowcolor0..7 take the pack's formats and clears
(white by default).
A shadow program that fails to compile leaves the pack without its
shadow map, with a warning; its terrain programs still run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pack's terrain program draws the solid sections the last test saw,
the boxes of the others are tested against the pack's depth in a pass
of their own (OpenGL's layout: y flipped, depth 0 near, so a second
depth-only culling pipeline), and the program then draws the sections
in sight from the culling's phase 2 draws. The flat path, which has no
occlusion culling, is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's Screen::blurs_background (in-world menu blur) has no arm for the
new Screen::Win. WinScreen.extractBackground fills the screen with the End
portal while the poem rolls and falls back to Screen.extractBackground
(blurred) for the credits alone, so Game::menu_blur blurs only for the
credits without the poem.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stacks #1197 on #1194 so it merges cleanly after it. game/packs.rs: both
PRs add one line to the resource reload; both are kept (waypoint styles,
then armour-stand trims).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/lan_e2e.py: main added the ladder and boats checks where this branch
added levels. Both are kept, with levels still last: it moves the players
between dimensions (and skips itself without --server-thread).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
hud.rs: HudBatch keeps both new fields, end_portal (this branch) and
blur_at (main's menu blur), in the struct, its constructor and the test
batch. draw_range already draws EndPortal runs on either side of blur_at.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When a Java pack has a terrain program (gbuffers_terrain, or the program
Iris falls back to), Horizon's world pass skips its own terrain and the
pack's program draws every visible section into its render targets,
tested against the depth of the rest of the world; gbuffers_water draws
the translucent layer before the first composite with the pack's blend
modes. depthtex0/depthtex1 get the depth before and after water.
- translate: a geometry mode (group 2, attributes as globals a glue
function fills before main, alpha test) and a pass that ends switch
cases which return, which naga's WGSL writer otherwise rejects
(Photon).
- java_terrain: the glue decoding Horizon's terrain vertices from the
arena pages into Iris's attributes, the block.properties id table by
sprite name, and a 2D atlas built from the sprite arrays (animated
sprites copied in).
- RGBA16/RG16/R16 targets are 16-bit unorm where the GPU renders them:
Chocapic13' packs its G-buffer into them.
MakeUp Ultra Fast, Chocapic13' and BSL draw their own terrain and water
on the M4 Pro; Photon still draws black (compute and .dat textures).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The comma form of modf (`v.y = trunc(x), x - trunc(x)`) printed without
brackets in a declaration's initialiser, which naga's GLSL front end then
read as a second declarator. A unary + around it has glsl-lang print the
brackets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The translator now legalises what these packs write and naga refuses:
lookups without a level outside fragment shaders become textureLod at
level 0; struct varyings travel as one varying per field; textureGather
is emulated with texelFetch; a bool given to a numeric constructor
becomes cond ? 1 : 0; `const in` parameters lose the const; array fields
of private structs get their natural stride (naga lays every struct out
as std140); modf into a vector component assigns it instead of passing a
pointer. shaders.properties keeps the empty lines the preprocessor
leaves, so Photon's `) \` before `#else` ends its value.
real_packs_load now also parses every translated program again as WGSL,
which is what wgpu is given.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column whose box lies entirely inside the frustum holds no section the
frustum would reject (plane distances only grow from its nearest corner
to a section's farthest, and rounding keeps the order), so its sections
skip the test. At render distance 32 most columns are past the distance
where the vertical field of view takes in their whole height.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every animation frame's mip chain goes to video memory once, at the
sprite's own size, as vanilla keeps its animated sprites' frames on the
GPU. A tick that changes sprites writes one 20-byte job per changed
sprite, and a compute dispatch per mip level repeats the chain into the
array's larger layers and mixes interpolated frames
(animate_sprite_interpolate), instead of blending on the CPU at the
array's size and copying each sprite level by level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
spawn_category_for_position looked up the picked entry's type twice per
attempt and asked mobsAt again at the position it had just picked the entry
from. The type is now resolved when the entry is picked, and the recheck is
skipped at that position (the list there holds the entry). The nearest player
distance only feeds comparisons with 24 and the despawn distances (128 at
most), so an attempt near its start measures only the players that can be
within 128 blocks of it; farther ones read as infinitely far, which every check
answers the same. The water-ambient biome tag is read only for that category.
Same lists, same random draws.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's player (a LAN guest, and since the server thread the single
player too) always respawned in the Overworld. Its respawn anchor was
dropped when its action ended, and its save kept only an Overworld point.
- The guest keeps a respawn point outside the Overworld (respawn_elsewhere,
saved as the player's `respawn`) and has it in place while it acts, so an
anchor it uses is its own.
- guest_respawn respawns it in that point's level, waits there for the
chunks, then resolves the point through its block
(findRespawnAndUseSpawnBlock): beside the anchor, using a charge, or the
world spawn and block.minecraft.spawn.not_valid when the block is gone.
- `spawnpoint` in another level than the Overworld sets a forced point there
(SetSpawnCommand) instead of Overworld coordinates, and a forced point that
is no anchor holds where a player fits (isPossibleToRespawnInThis), in the
window's own game too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
handle_server_portal ignored End gateways, so on a server (dedicated, or
single player's server thread) no player could use one: the in-frame game's
gateway code moved only its own player.
A gateway that is not cooling down now takes a server player at once
(EndGatewayBlock.entityInside, triggerCooldown, setAsInsidePortal), within
the End, keeping its motion and look (Relative.DELTA and ROTATION). An exact
exit is reached straight away; otherwise the player waits at the destination
while it loads, and the exit is found or built there (the in-frame
resolve_gateway, shared through gateway_job).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (dedicated, or single player's server thread) each dimension is
its own level, but a command ran in whichever level was in place. So a guest
in the Nether running `execute in minecraft:overworld run setblock ...` got
"That position is not loaded", and `execute at <a player in another level>`
used the wrong level's blocks.
with_source now puts the source's level in place for the command
(CommandSourceStack.getLevel), as as_target already does for a player in
another level, and puts the old one back after. `execute at` takes the
entity's level (withLevel(entity.level())), and a player's level is the
guest's own (target_level) even while another level is in place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server the guest is held where it entered the Nether until the
chunks around the destination are lit, then placed in its portal. The
check read the guest's position 40 ticks after the dimension changed,
which on a busy machine was still the held spot, and then stepped the
guest out of a portal that wasn't there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new `levels` check (--server-thread) runs the host and a guest in different
dimensions: execute in ... tp, mobs ticking in two levels at once with unique
ids, rejoin and save/reload with the guest in the Nether, death there, the
host through a Nether portal alone, the guest through an End portal and the
exit portal alone. All of it already works on the server thread.
docs/server-levels.md says how the server's levels map onto vanilla's, what
the dedicated server and the server thread share, the cost of an extra level
and what's left. docs/server.md no longer says players can't change dimension.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_chunk_random asks for the lowest randomly ticking section from a height
up, instead of asking about every section: a chunk costs one question per
section that ticks, not 24. PalettedContainer::has tests whole words for the
few matching palette indices (a field-wise zero test) instead of reading
every entry, so a section read again after an edit costs a few hundred word
operations.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every font definition of the resources (and packs) is now loaded, not
just font/default.json, and a text component's "font" field picks one:
"minecraft:alt" draws the enchanting table's Galactic alphabet,
"minecraft:illageralt" the illager runes, a pack's own font its glyphs,
and a font that doesn't exist draws every character as the missing
glyph (FontManager.getFontSetRaw).
- font.rs: one FontSet per font id with its providers in order (space,
bitmap, unihex, ttf) and their filters ("uniform", "jp"). Bitmap
sheets outside the atlas (another namespace or folder) are copied into
the glyph region when first drawn.
- TrueTypeGlyphProvider: "ttf" providers (file under font/, size,
oversample, shift, skip; CFF fonts refused) rasterized with ab_glyph
into the glyph region as rrrr coverage (text_intensity), metrics as
GlyphBitmap (bearings over the oversampling, whole-pixel advances).
- text::Style gains the font id; it is parsed from JSON and SNBT
components, pack descriptions and scoreboard number formats, inherited
by children and written back.
- Obfuscated text picks its random glyph in the style's font.
Fixes#336
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Leaving the End through the exit portal for the first time now rolls the
end poem, the credits and the post-credits text (WinScreen), as does a
server's WIN_GAME event; Options -> Credits & Attribution -> Credits rolls
the credits alone. The texts come from the resource root
(texts/end.txt, credits.json, postcredits.txt), so packs can replace them.
- win_screen.rs: vanilla's line building (PLAYERNAME, the obfuscated
names from LegacyRandom(8124371), wrapping at 256), scroll speeds and
keys (Space, Ctrl, Up, Escape), the last line pinned mid-screen, the
logo, the vignette and the backgrounds.
- HUD: Blend::Vignette (RenderPipelines.VIGNETTE) and Blend::EndPortal
(the End portal effect as a GUI fill, the poem's background).
- Obfuscated text draws a random glyph of the same width each frame.
- seenCredits is read and saved; the player stays out of the world while
the poem rolls and arrives in the Overworld after it.
- horizon-ctl `credits [poem] [SCROLL]` and `options credits`.
Fixes#338
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla random-ticks a section while its count of randomly ticking blocks
and fluids is above zero (LevelChunkSection.isRandomlyTicking), and asks as
it reaches each section. Horizon ticked every section whose palette listed
such a state, even after the last one was gone, and decided before the tick's
scheduled ticks, which drew positions vanilla doesn't and shifted its
position sequence.
RandomTickSections now reads each section's blocks (PalettedContainer::has),
again only for the sections that changed, and tick_chunk_random asks for
each section when it reaches it; the game reads a chunk's sections again only
after a block changed. New harness random_tick_sections compares the
sections and positions with vanilla's real section counts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocatorBar: when the server tracks waypoints for the player, the bar
above the hotbar shows a dot per waypoint within 60 degrees of the
view (farthest first), coloured from the waypoint's colour or its id's
hash, with the sprite its style picks by distance
(assets/*/waypoint_style/*.json, WaypointStyleManager) and the blinking
up or down arrow when the waypoint is above or below the screen. The
background is nine-sliced. Hud.nextContextualInfoState picks between
the jump bar, the experience bar (100 ticks after an experience change)
and the locator bar.
Waypoints come from ClientboundTrackedWaypointPacket on remote servers
and, on a served world, from the host's own connection in
lan/waypoints.rs; entity waypoints in view follow the entity's eyes.
Fixes#339
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EquipmentLayerRenderer's trim layer: the pattern's
textures/trims/entity/<layer>/<asset>.png recoloured from its base
palette (palette metadata section) to the material's palette, or to the
equipment asset's trim_overrides palette (iron armour with an iron trim
uses trim/iron_darker), as PalettedTextureManager does. Each texture is
made once, on first use, into one of 32 atlas trim slots (least recently
drawn reused), and drawn over the armour layer on the player, on the
guests a LAN host sees and on armour stands.
Fixes#337
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TextureMetadataSection's blur and clamp choose how vanilla samples a
standalone texture (ReloadableTexture.setSampler). The atlas now keeps
each texture's flags and the screen-effect quads (vignette, pumpkin blur,
powder snow outline, underwater, equippable camera overlays) sample
their sprite rectangle linearly and with clamp or repeat as those flags
say, instead of always nearest and repeat.
Fixes#335
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reduced starts each far tile level at half the distance Full does (about a
quarter of the far quads beyond the full-detail edge, whose finest tiles stay).
Full stays the default everywhere and no preset changes it. On integrated
GPUs other than Apple's, the setting's description suggests Reduced.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A list sorted only by distance switched vertex buffer every few sections:
about 1200 indirect multi-draws a frame at render distance 32, most of a
few entries, which Mesa's Intel driver unrolls on the CPU. Grouping each
band's sections by page keeps the list front to back within the band, with
one call per page and band.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:37:40 +02:00
1374 changed files with 72566 additions and 34914 deletions
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
## Crates
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~190 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
| `horizon-telemetry`, `horizon-telemetry-server`, `horizon-bench` | Opt-in data collection, its ingest server, the benchmark report format |
@@ -45,7 +46,7 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **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.
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining 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).
@@ -57,7 +58,7 @@ Screenshots, captured in the Rust client itself with vanilla assets and no mods
</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/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></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>
@@ -109,6 +110,10 @@ Everything prepared lands in the gitignored `.local/`. It is derived from the ga
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, scripting through `horizon-ctl` and the architecture are described in [the native client guide](docs/native-client.md). The rest of the developer documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Roadmap
What is being done now, what comes next and where you can help, in plain words and in technical detail: [ROADMAP.md](ROADMAP.md). [playhorizon.dev/hub](https://playhorizon.dev/hub) shows it with the work board, the journal of what shipped and how to help.
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels), or [the contributing guide](https://playhorizon.dev/docs/contributing) on the site. Questions and ideas: [Discord](https://discord.gg/HcTeMZxe5E). Before opening a pull request, run the self-check and paste the block it prints:
<!-- Generated by tools/roadmap.py from roadmap/roadmap.toml: edit that file, then run `python3 tools/roadmap.py build`. -->
What is being done on Horizon, what comes next and where you can help. There are no dates: things are Now, Next or Later. Each item is said in plain words, with the technical picture folded under it. The same roadmap, with a Simple / Technical switch, the board, the journal and how to help, is on [playhorizon.dev/hub](https://playhorizon.dev/hub). Pull requests carry the `roadmap:<id>` label of their item.
## At a glance
Horizon is Minecraft: Java Edition 26.3 rewritten from scratch in Rust, so it runs faster on the same computer. Single player, LAN, servers, resource packs and Horizon's own mods work today, on your own copy of the game.
**Being done now**
- **Steady frames at long render distances**: Making Horizon faster than vanilla on every computer, with smooth camera movement even at 32 chunks, by spreading the work better over the processor's cores.
- **Modpacks' world generation, natively**: Making the world-generation mods of popular modpacks (new structures, YUNG's dungeons and mineshafts, packs built on Lithostitched) generate their worlds in Horizon, natively and fast.
- **Playing on servers: everything shows as it should**: When you play on a server, other players, mobs, items and menus look and behave as in vanilla: skins, poses, item names, inventories and server networks.
- **Single player as a server: the last differences**: Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
- **Horizon's mod registry**: One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
**Next**: Microsoft sign-in (coming soon); CurseForge in the mod browser (coming soon); Fresh benchmarks against vanilla; Pistons push players and mobs; Horizon on Intel Macs; Faster world creation and entry on small CPUs; Servers that hold 20 TPS with 50 to 100 players; Low-end computers: Steam Deck, old Macs, 8 GB; Native upscaling; A batch of targeted optimisations; A Convert to Rust button for what isn't rewritten yet; Mods with code: machines, magic, mobs; Data packs in the mod browser; Prepare a version without Java; Server console, bans and a full admin API; Hosting polish: Windows service, safe updates; Guided hosting with Tailscale and an invite button; An official Docker image for servers; One single-player path: remove the in-frame world; Villages and combat, exactly as in vanilla; Choose the audio output device; Mods that read and change the world; Machines and blocks with inventories (API 0.4); Lua and other languages for mods; Well-known shader looks, rewritten for Horizon; A visible beta label; Wayland and Hyprland polish; A versioned agent-control protocol; Port mods with your own AI agent; Horizon on X (coming soon); Pipeline protection and a GitHub App for agents; Signed updates everywhere, installers included.
**How to help**: Play it and tell us what differs from vanilla (Discord or GitHub), run the benchmark on your computer, vote for what you want next, or pick one of the Help wanted items.
**Developers**: Read CONTRIBUTING.md, pick a Help wanted item or a To do card, check vanilla's code for the behaviour, and paste the block `python3 tools/pr_ready.py` prints in your pull request.
## Now
### Performance
#### Steady frames at long render distances
Making Horizon faster than vanilla on every computer, with smooth camera movement even at 32 chunks, by spreading the work better over the processor's cores.
<details><summary>Technical</summary>
Frame pacing at render distance 32 ([#678](https://github.com/ludovic111/horizon/issues/678)): long frames come from work competing with the render thread. In review: world generation workers build free structure starts instead of queueing them ([#1760](https://github.com/ludovic111/horizon/issues/1760)), the worker pool leaves one core as vanilla does ([#1778](https://github.com/ludovic111/horizon/issues/1778)), and the terrain vertex arena is read as storage where flat draws run ([#1742](https://github.com/ludovic111/horizon/issues/1742)). Still open: far terrain at 48 to 64 chunks ([#718](https://github.com/ludovic111/horizon/issues/718)) and CPU-bound fields of block entities ([#717](https://github.com/ludovic111/horizon/issues/717)). Every change shows before and after numbers from `tools/perf_bench.py` against vanilla 26.3 ([docs/benchmark.md](docs/benchmark.md)).
</details>
#### Faster block lookups
Speeding up the code that reads and writes the blocks of the world, which almost everything in the game goes through: generation, lighting, meshing and ticking.
<details><summary>Technical</summary>
`PalettedContainer` and `PackedStorage` find an entry's word by multiply and shift instead of an integer division ([#1790](https://github.com/ludovic111/horizon/issues/1790), [#1794](https://github.com/ludovic111/horizon/issues/1794)); meshing looks up baked models by index, not by name ([#1747](https://github.com/ludovic111/horizon/issues/1747)). Each change keeps the paletted storage harnesses identical and shows its microbenchmark ([docs/performance/paletted-container-access-results.md](docs/performance/paletted-container-access-results.md)).
</details>
### Modpacks
#### Modpacks' world generation, natively
Making the world-generation mods of popular modpacks (new structures, YUNG's dungeons and mineshafts, packs built on Lithostitched) generate their worlds in Horizon, natively and fast.
<details><summary>Technical</summary>
`horizon-port-pack` carries a pack's data-driven content over; structure mods that need code get a native port: YUNG's worldgen as written ([#1746](https://github.com/ludovic111/horizon/issues/1746)), Lithostitched's limited and guaranteed elements with pool and processor modifiers ([#1777](https://github.com/ludovic111/horizon/issues/1777)), its processors as the game's own types ([#1765](https://github.com/ludovic111/horizon/issues/1765)). Proved by the structure blocks and layout harnesses ([#1753](https://github.com/ludovic111/horizon/issues/1753), [#1757](https://github.com/ludovic111/horizon/issues/1757)). Mods whose Java code does more than world generation don't run ([docs/modpacks.md](docs/modpacks.md)).
</details>
### Servers and multiplayer
#### Playing on servers: everything shows as it should
When you play on a server, other players, mobs, items and menus look and behave as in vanilla: skins, poses, item names, inventories and server networks.
<details><summary>Technical</summary>
Client-of-a-server fixes in `game/remote*.rs` and `horizon-protocol`: mobs' aggressive and item-use flags ([#1802](https://github.com/ludovic111/horizon/issues/1802), [#1798](https://github.com/ludovic111/horizon/issues/1798)), ghost items after a remote menu ([#1795](https://github.com/ludovic111/horizon/issues/1795)), skins ([#1652](https://github.com/ludovic111/horizon/issues/1652)), cookies and transfer packets for proxy networks ([#1650](https://github.com/ludovic111/horizon/issues/1650)), firework boosts ([#1671](https://github.com/ludovic111/horizon/issues/1671)), living-entity attributes and simulation distance ([#1731](https://github.com/ludovic111/horizon/issues/1731)), styled item names ([#1702](https://github.com/ludovic111/horizon/issues/1702)), primed TNT between updates ([#1710](https://github.com/ludovic111/horizon/issues/1710)).
</details>
#### Server dialogs and login screens
Servers that show dialogs, such as the register and login screens of AuthMe, will work in Horizon.
<details><summary>Technical</summary>
26.3's dialog packets and model ([#1720](https://github.com/ludovic111/horizon/issues/1720)), the dialog screen with its buttons and actions ([#1734](https://github.com/ludovic111/horizon/issues/1734)), then text and boolean inputs ([#1735](https://github.com/ludovic111/horizon/issues/1735)).
</details>
#### Pre-generate an area with /pregen
A /pregen command that builds an area of the world ahead of time, so players don't wait for new terrain when they explore.
<details><summary>Technical</summary>
Generates, lights and saves an area before anyone walks there ([#1805](https://github.com/ludovic111/horizon/issues/1805), a contributor's pull request, in review). Useful on servers, and it moves the generation cost away from the first visit; the generation speed-ups are under 'Steady frames at long render distances'.
</details>
### Mods and API
#### Horizon's mod registry
One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
<details><summary>Technical</summary>
Merged: the registry index (schema 1, `.hpack`, checks before install, [#1634](https://github.com/ludovic111/horizon/issues/1634)), `cargo horizon-mod check` ([#1642](https://github.com/ludovic111/horizon/issues/1642)) and the submission service (`crates/horizon-mod-registry`, [#1659](https://github.com/ludovic111/horizon/issues/1659)). In review: the website's /mods ([#1596](https://github.com/ludovic111/horizon/issues/1596)) and /mods/submit ([#1635](https://github.com/ludovic111/horizon/issues/1635)), and in the launcher the registry as a source ([#1636](https://github.com/ludovic111/horizon/issues/1636)), install, enable and takedowns ([#1637](https://github.com/ludovic111/horizon/issues/1637)), the Mods page ([#1638](https://github.com/ludovic111/horizon/issues/1638)) and modpacks as their own profiles ([#1640](https://github.com/ludovic111/horizon/issues/1640)).
More: [docs/mods.md](docs/mods.md)
</details>
#### Mods that run on the server
Mods that run on the server side of a world, so a dedicated server or a LAN world can have mods that react to players joining, chat and blocks being broken or placed, and that add commands.
<details><summary>Technical</summary>
API 0.3, experimental first ([#1833](https://github.com/ludovic111/horizon/issues/1833), in review): the same sandboxed component runs on the server thread and the dedicated server, with world events (block broken or placed, entity spawned or killed, player joined or left), game rules and server commands with typed arguments. The world, item and entity interfaces come after (see 'Mods that read and change the world'). [docs/api/mods.md](docs/api/mods.md), [docs/mods.md](docs/mods.md).
</details>
#### Screens, input and drawing for mods
Mods will be able to open their own screens and settings pages and react to keys and item use, so bigger mods, such as the Orbital Laser, can run on the normal game.
<details><summary>Technical</summary>
Capability `ui` is in review ([#1864](https://github.com/ludovic111/horizon/issues/1864)): mod-owned screens and a settings page under Options > Mods, built from vanilla's widgets. Still to design: `input` (an item's use, key binds, the crosshair target), `render` (a budgeted draw list on the native shader API) and `world` (changing blocks). Orbital Laser is the acceptance test: it needs all four today through its author's fork (see 'Orbital Laser on the normal game').
More: [docs/mods.md](docs/mods.md)
</details>
#### Tools for people who make mods
Better tools for mod makers: a test runner that needs no game window, templates for each kind of mod, and clear promises about what keeps working from one version to the next.
<details><summary>Technical</summary>
`cargo horizon-mod test` runs a mod's test scripts against a fake game snapshot ([#1816](https://github.com/ludovic111/horizon/issues/1816)) and, for server mods, scripted server events ([#1837](https://github.com/ludovic111/horizon/issues/1837)); the content template and the guides are merged ([#1859](https://github.com/ludovic111/horizon/issues/1859), [#1810](https://github.com/ludovic111/horizon/issues/1810), [docs/api/stability.md](docs/api/stability.md)). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference entry is removed without a new major version, and the SDK on crates.io once the API is stable.
#### Single player as a server: the last differences
Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
<details><summary>Technical</summary>
Since [#1514](https://github.com/ludovic111/horizon/issues/1514) single player runs on the server thread (`game/integrated.rs`) and the window joins it as a client; `--in-frame` keeps the old path. The open `[server-thread]` issues list the gaps. In review: a tool that changes a block ends the click ([#1797](https://github.com/ludovic111/horizon/issues/1797)), potions keep their potion over the connection ([#1796](https://github.com/ludovic111/horizon/issues/1796)), mobs' aggression and arm swings ([#1802](https://github.com/ludovic111/horizon/issues/1802)). [docs/server-levels.md](docs/server-levels.md).
</details>
#### Play in your language
Item names, container titles and server messages follow the language you pick, as in vanilla. Today parts of the game stay in English.
<details><summary>Technical</summary>
Translatable components resolved through the selected language for items, containers and server text ([#1806](https://github.com/ludovic111/horizon/issues/1806)), on top of the bundled languages the prepared resources carry ([#1594](https://github.com/ludovic111/horizon/issues/1594)). Left: the client's own screens ([#1672](https://github.com/ludovic111/horizon/issues/1672)) and the main menu with zh_cn ([#1774](https://github.com/ludovic111/horizon/issues/1774)).
</details>
### Rendering
#### Shader packs, written again from scratch
Shader packs were removed and are being rebuilt from scratch. They come back, off by default, once the best-known packs draw properly.
<details><summary>Technical</summary>
A clean-room rewrite in `horizon-shaderpack`, from the packs' public format documentation only: reading packs, the preprocessor and `.properties` files are merged ([#1584](https://github.com/ludovic111/horizon/issues/1584), [#1585](https://github.com/ludovic111/horizon/issues/1585), [#1586](https://github.com/ludovic111/horizon/issues/1586)); translating programs to WGSL through naga, buffers, uniforms and the terrain, entity, sky, weather and hand stages are in review ([#1698](https://github.com/ludovic111/horizon/issues/1698) to [#1801](https://github.com/ludovic111/horizon/issues/1801)).
Horizon's own way to make shader packs, with tools to write and test them. Every pack is checked before it runs, and the registry will list them.
<details><summary>Technical</summary>
`horizon-shader-api` (API 0.1, experimental): WGSL stages with typed inputs inside the renderer's passes, options as WGSL override constants, and the `horizon-shader new|check|pack` tool with templates and the example pack Golden Hour. Merged: the check every pack passes ([#1820](https://github.com/ludovic111/horizon/issues/1820)) and the tool, templates and guide ([#1822](https://github.com/ludovic111/horizon/issues/1822)). In review: the post stage running in the game with hot reload ([#1861](https://github.com/ludovic111/horizon/issues/1861)), the sky stage ([#1863](https://github.com/ludovic111/horizon/issues/1863)) and registry checks that use the game's own check ([#1855](https://github.com/ludovic111/horizon/issues/1855)). Left: the material, lighting and shadow stages, a last-good fallback on reload, a GPU-time overlay per stage and budgets ([docs/shaders.md](docs/shaders.md), [docs/api/shaders.md](docs/api/shaders.md)). The Iris/OptiFine loader is a separate item, 'Shader packs, written again from scratch'.
</details>
### Horizon extras
#### Discord Rich Presence
Your Discord profile can show that you are playing Horizon, with a switch to turn it off.
<details><summary>Technical</summary>
A Discord IPC client on its own thread ([#1787](https://github.com/ludovic111/horizon/issues/1787)) and the presence with its setting ([#1788](https://github.com/ludovic111/horizon/issues/1788)). Merged: it ships with the next release.
</details>
### Code and tools
#### A leaner codebase that builds faster
Cleaning up Horizon's code: removing what nothing uses and making it quicker to build, so fixes and new features land faster.
<details><summary>Technical</summary>
A review of the whole tree: dead code and stubs removed ([#1432](https://github.com/ludovic111/horizon/issues/1432), [#1658](https://github.com/ludovic111/horizon/issues/1658)), one probe binary per crate ([#1453](https://github.com/ludovic111/horizon/issues/1453), [#1454](https://github.com/ludovic111/horizon/issues/1454)), shared harness helpers ([#1448](https://github.com/ludovic111/horizon/issues/1448)), clippy clean on the whole tree ([#1707](https://github.com/ludovic111/horizon/issues/1707)). [docs/architecture.md](docs/architecture.md) maps each feature to its files and harnesses.
</details>
## Next
### Performance
#### Fresh benchmarks against vanilla
New public numbers comparing Horizon with vanilla on many kinds of computers, measured the same way every time, with a history that shows what each release changed.
<details><summary>Technical</summary>
Redo the comparison after the current performance work ([#1073](https://github.com/ludovic111/horizon/issues/1073)); reliability, coverage and presentation ([#1346](https://github.com/ludovic111/horizon/issues/1346)); a scene must count as loaded only once its first chunk arrives ([#1749](https://github.com/ludovic111/horizon/issues/1749)). Next: a performance history across releases (an automatic comparison of each release with the last, on a timeline), more report fields (server TPS and MSPT, power, battery drain, temperature, throttling) and more hardware (a MacBook on battery, low-end laptops, community reports). Players' reports come in through `--benchmark` and `horizon-launcher data benchmark` ([docs/benchmark.md](docs/benchmark.md)).
</details>
#### Faster world creation and entry on small CPUs
Creating a world and joining it should be quick even on a four-thread laptop, where a fresh world at 32 chunks can make you wait minutes for the first terrain.
<details><summary>Technical</summary>
Cut the per-column generation cost (noise sampling, surface rules, aquifers) and the lock convoy on structure starts, and leave one core free for the rest of the game as vanilla does ([#1760](https://github.com/ludovic111/horizon/issues/1760) and [#1778](https://github.com/ludovic111/horizon/issues/1778), in review, are listed under 'Steady frames at long render distances'). Still to profile on small CPUs: the first seconds of a fresh world deliver no chunk, and fluids remesh for about a minute after loading. Measured with `tools/perf_bench.py` ([docs/benchmark.md](docs/benchmark.md), [docs/profiling.md](docs/profiling.md)).
</details>
#### Servers that hold 20 TPS with 50 to 100 players
Servers that keep 20 ticks per second with 50 to 100 players. Today Horizon holds it up to 50 bots and drops to about 10 at 100.
<details><summary>Technical</summary>
Measured against vanilla on equal work ([docs/server.md](docs/server.md), Scale): Horizon is ahead with few players and when exploring, but vanilla's tick is shorter with 25 to 50 players near spawn. Ideas on the list: send entity changes instead of rebuilding every entity each tick (about 15 percent), fewer memory reads in random ticks and collision scans, cheaper item collision checks, mob AI and natural spawning. The bot benchmark (`horizon-bots`) is the proof.
</details>
#### Low-end computers: Steam Deck, old Macs, 8 GB
Horizon should run well on small machines: integrated graphics, the Steam Deck, older Macs and 8 GB of memory, with a battery mode that saves power on laptops.
<details><summary>Technical</summary>
Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far Detail Reduced for integrated GPUs and animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported on the Benchmarks page.
Draw the game at a lower resolution and sharpen it to your screen's size, in the way of FSR or DLSS, to gain frames on weaker graphics cards. An optional graphics setting.
<details><summary>Technical</summary>
A render-scale setting below the window size with an upscaler of our own that runs on every wgpu backend. Proof: frame time against native resolution on integrated GPUs, and screenshots for image quality. Issue [#1403](https://github.com/ludovic111/horizon/issues/1403).
</details>
#### A batch of targeted optimisations
Several smaller speed-ups, each measured: idle block entities that sleep, cheaper particles, faster structure placement, high-rate mice and very large texture packs.
<details><summary>Technical</summary>
Block entities and hoppers that skip work when idle (the hopper item scan is already skipped when no item is near); a particle frustum test first (also a parity gap), then particle quads built on the GPU; jigsaw shortcuts that skip blocked connectors and pool elements known not to fit, with layouts unchanged (trial chambers and ancient cities first); batched raw mouse input on Windows for 8000 Hz mice; a better atlas packer for huge resource packs (a 64x pack builds a 704 MiB atlas for 452 MiB of textures); sign-heavy scenes ([#1461](https://github.com/ludovic111/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
More: [docs/profiling.md](docs/profiling.md)
</details>
### Modpacks
#### CurseForge in the mod browser (coming soon)
Coming soon: search and download CurseForge mods and modpacks from the launcher, as you can with Modrinth today.
<details><summary>Technical</summary>
The mod browser lists CurseForge as coming soon ([docs/mod-browser.md](docs/mod-browser.md)): it needs CurseForge API access. Meanwhile packs import from a CurseForge zip or instance folder, and jars only CurseForge hosts come from the player's own instance ([docs/modpacks.md](docs/modpacks.md)).
</details>
#### A Convert to Rust button for what isn't rewritten yet
When a mod, shader pack or modpack hasn't been rewritten for Horizon, one button converts what can be converted, so you don't have to wait for the rewrite.
<details><summary>Technical</summary>
Generalise the mod browser's Port step (`horizon-port-pack`: the data, assets, worldgen and block and item definitions of Java mods, .mrpack and CurseForge zips) to every kind of content: code mods where the API allows it (see 'Mods with code') and shader packs, converting an Iris/OptiFine pack into Horizon's format. The browser says what works before the install. Java code itself does not run ([docs/modpacks.md](docs/modpacks.md), [docs/mod-browser.md](docs/mod-browser.md)).
</details>
#### Mods with code: machines, magic, mobs
Making mods that add machines, magic and mobs work in Horizon, in steps, starting with machines. They are most of what a big modpack is made of.
<details><summary>Technical</summary>
333 of the 496 mods in a big pack (ATM10) contain code. Issue [#1854](https://github.com/ludovic111/horizon/issues/1854) ranks the API gaps (ticking block entities, networking, rendering, menus, inventories) that unlock them in tiers, with machines first as the recommended path and a furnace ported tick for tick as proof. The first API step is a draft ([#1189](https://github.com/ludovic111/horizon/issues/1189), see 'Machines and blocks with inventories'). The path isn't started yet ([docs/modpacks.md](docs/modpacks.md)).
</details>
#### Data packs in the mod browser
Search and install data packs from the launcher's mod browser, for the world you choose, as you do mods and resource packs.
<details><summary>Technical</summary>
A Datapacks tab beside Mods and Resource packs: install into a chosen world's datapacks folder, with the same checks and 'works in Horizon' notes as the other kinds ([docs/mod-browser.md](docs/mod-browser.md)).
</details>
### Servers and multiplayer
#### Prepare a version without Java
Set up Horizon, a server first, without Java on your computer: published tables are combined with your own copy of the game.
<details><summary>Technical</summary>
Today `horizon-prepare` runs Mojang's exporters with Java. Issue [#1750](https://github.com/ludovic111/horizon/issues/1750) proposes publishing the computed version tables once from CI and assembling the rest locally from the player's own jar and assets, provided the tables hold no Mojang content; a contributor offered a pull request, and it would help phones too. A server-only prepare from `server.jar` alone is the first step ([docs/server.md](docs/server.md)).
</details>
#### Server console, bans and a full admin API
Manage a server from a console, ban and pardon players, and drive it from other tools through a full admin API and an MCP server for AI agents.
<details><summary>Technical</summary>
`horizon-server console` (a text interface), `ban`, `pardon`, `ban-ip` and `mcp` over stdio. Admin API additions: the player list, kick, ban, op and whitelist, an SSE stream of logs and events, a text metrics endpoint, config GET and PATCH, backup delete and download. The panel's later features (bans, worlds, server.properties import, logs and crash reports, modpacks) and vanilla's management protocol (the JSON-RPC over WebSocket that the `management-server-*` keys configure), so vanilla's tools can manage a Horizon server. [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
</details>
#### Hosting polish: Windows service, safe updates
Smoother hosting: a real Windows service, a backup before every update, backups that keep the world and each player's state, and a changed view distance or message without a restart.
<details><summary>Technical</summary>
A Windows service wrapper instead of the logon scheduled task; `backups.before_update`; hot reload of config for view distance and the message of the day (needs engine support); backups that keep world and player state ([#1808](https://github.com/ludovic111/horizon/issues/1808), a contributor's draft, in review). [docs/hosting.md](docs/hosting.md), docs/server.md.
</details>
#### Guided hosting with Tailscale and an invite button
In the launcher, a guide and a button to host a server and invite friends over Tailscale, so nobody has to open ports on a router.
<details><summary>Technical</summary>
On the Servers page (create, start, back up, update and invite are shipped) add a Tailscale setup that explains and checks each step, and an invite button that hands friends what they need to join over the tailnet. Buttons first, no docs needed, with the details in an Advanced section. Remote access over Tailscale is already described in [docs/server.md](docs/server.md).
More: [docs/hosting.md](docs/hosting.md)
</details>
#### An official Docker image for servers
An official image to run a Horizon dedicated server in Docker with one command, your own copy of the game files mounted in.
<details><summary>Technical</summary>
The repository has the Dockerfile (`packaging/server/Dockerfile`: a multi-stage build on Debian bookworm-slim, user `horizon`, volume `/data`, port 25565, game files mounted read-only, `HORIZON_EULA` and friends answering the first-start questions), and anyone can build it ([docs/hosting.md](docs/hosting.md), section 9). Left: publish the image to a public registry from the release workflow, tagged per release, so it follows the same checks, and update the docs and the launcher. The image never holds Mojang's files.
More: [docs/server.md](docs/server.md)
</details>
### Mods and API
#### Mods that read and change the world
Mods that can see and change blocks, read inventories and items, and find and affect entities, all through the server so every player sees the same thing.
<details><summary>Technical</summary>
After server mods: read blocks, states and biomes in loaded chunks and change them through the server's block log, so clients and the parity harnesses see vanilla's updates; the player's inventory, held item, open container and item stacks with components; entity queries near a position, a safe subset of entity data and data-driven mod entity types. Snapshots and batched calls per tick, never per-block host calls in hot paths. [docs/api/mods.md](docs/api/mods.md) ('Not there yet').
More: [docs/modpacks.md](docs/modpacks.md)
</details>
#### Machines and blocks with inventories (API 0.4)
Mods that add machines: blocks that tick, hold items and have their own menus. This unlocks the technology mods of big modpacks.
<details><summary>Technical</summary>
Block entities and menus: ticking machines with slots, server menu logic and the menu protocol to clients, energy, item and fluid transfer, data attachments, accessory slots and custom recipe types, with a typed client and server channel and typed config beside them. A draft ([#1189](https://github.com/ludovic111/horizon/issues/1189)) proves a furnace ported tick for tick and holds the ATM10 code-mods plan; the design is redone through the API policy before it merges ([docs/modpacks.md](docs/modpacks.md), docs/api/stability.md).
</details>
#### Lua and other languages for mods
Write mods as Lua scripts, in the style of KubeJS, with no compiler, and later in other languages that pass the tests. Every language runs in the same sandbox.
<details><summary>Technical</summary>
Lua first: Horizon ships a Lua interpreter that is itself a WebAssembly mod, and a script mod's manifest names it and the capabilities it uses, so a script can do exactly what a compiled mod with the same manifest can, with hot reload of `main.lua`. Then C, C++, Zig, Go (TinyGo), AssemblyScript and JavaScript or TypeScript are measured against Rust on the same tick-heavy example, and each is supported only with a template, an example and a CI test. [docs/api/mods.md](docs/api/mods.md) ('Ways in').
</details>
### Gameplay parity
#### Pistons push players and mobs
Pistons and slime blocks will push players, mobs and items as in vanilla. Today moving blocks pass through them.
<details><summary>Technical</summary>
Port `PistonMovingBlockEntity.moveCollidedEntities` and `moveStuckEntities` (honey), the slime bounce and `Entity.limitPistonMovement` for `MoverType.PISTON`, in single player and for a server's players. Issues [#1496](https://github.com/ludovic111/horizon/issues/1496), [#1609](https://github.com/ludovic111/horizon/issues/1609), [#1608](https://github.com/ludovic111/horizon/issues/1608), [#1704](https://github.com/ludovic111/horizon/issues/1704), [#1758](https://github.com/ludovic111/horizon/issues/1758).
</details>
#### One single-player path: remove the in-frame world
Single player will have one way to run, the server thread, so every fix works everywhere and there are fewer bugs. The older in-frame mode goes once everything passes without it.
<details><summary>Technical</summary>
`--in-frame` (also `--no-server-thread` and `HORIZON_SERVER_THREAD=0`) still ticks the world inside the window's `Game`, and most harnesses test that path. Delete it once `lan_e2e --server-thread` and the parity harnesses pass on the server thread, which also halves the bug surface. See also 'Single player as a server' and [docs/server-levels.md](docs/server-levels.md).
</details>
#### Villages and combat, exactly as in vanilla
Villages and fights behaving as in vanilla: iron golems defending villages, villagers remembering their homes, mace smashes and knockback.
<details><summary>Technical</summary>
Villages: iron golem village goals, villager memories across saves, villager age, job-site claiming on dedicated servers, flower offers ([#1639](https://github.com/ludovic111/horizon/issues/1639) is one report). Combat: the mobs' `attack_knockback` attribute, mace smash logic, the hit knockback source on the host, the fire burnout biome tag. Each is checked against vanilla by a harness ([docs/rust-parity.md](docs/rust-parity.md)).
</details>
#### Choose the audio output device
Pick the speakers or headphones Horizon plays through, in Music & Sounds.
<details><summary>Technical</summary>
An output device list in Music & Sounds, falling back to the system default. [#1035](https://github.com/ludovic111/horizon/issues/1035) started it: a player on an older Windows computer had no sound at all, and a device choice is how such cases are found and fixed.
</details>
### Rendering
#### Well-known shader looks, rewritten for Horizon
Popular shader packs rewritten in Horizon's format, so good packs exist from the start. Only packs whose licence is open source and compatible with ours are rewritten, never anything else.
<details><summary>Technical</summary>
The first candidates are Photon, Chocapic13 and MakeUp Ultra Fast, each only after its licence is read and recorded: a pack is rewritten only if it is open source and compatible with Horizon's licence, and the rewrite is written from the shader format's documentation and the look on screen, not from anyone's code. Today only the Golden Hour example exists (`[crates/horizon-shader-api/packs/golden_hour](crates/horizon-shader-api/packs/golden_hour)`).
More: [docs/shaders.md](docs/shaders.md)
</details>
### Launcher
#### Microsoft sign-in (coming soon)
Coming soon: sign in with your Microsoft account to play on online-mode servers. The code is ready; it waits for Microsoft's approval.
<details><summary>Technical</summary>
Device-code sign-in in the launcher and online-mode login in the client ([docs/microsoft-account.md](docs/microsoft-account.md)). It is turned on by setting the app's client id once Minecraft API access is approved; until then the launcher shows "Coming soon".
A clear 'beta' label in the launcher and the game, so nobody is surprised that Horizon is still being built.
<details><summary>Technical</summary>
Show Beta in the launcher's header and about page, on the game's title screen and on the website, beside the version number ([docs/versioning.md](docs/versioning.md)).
</details>
### Platforms
#### Horizon on Intel Macs
Builds of the game and the launcher for Macs with an Intel processor.
<details><summary>Technical</summary>
x86_64 game and launcher builds, picked per processor ([#1171](https://github.com/ludovic111/horizon/issues/1171)). Before it ships it needs a run on a real Intel Mac: Rosetta on Apple Silicon only proves the CPU code. Requests: [#1161](https://github.com/ludovic111/horizon/issues/1161), [#1792](https://github.com/ludovic111/horizon/issues/1792).
</details>
#### Wayland and Hyprland polish
Fixes for Linux desktops that use Wayland and Hyprland: fullscreen, the clipboard, the GUI scale after a restart and the like.
<details><summary>Technical</summary>
Open: the GUI Scale is clamped to 2 after a restart under Hyprland, while XWayland keeps it ([#1866](https://github.com/ludovic111/horizon/issues/1866)). Earlier reports covered fullscreen dropping on menu clicks, the clipboard and a white overlay ([#1007](https://github.com/ludovic111/horizon/issues/1007), closed). Checked on a real Wayland session.
</details>
### AI and agents
#### A versioned agent-control protocol
A stable, documented way for AI agents and scripts to drive the game and the launcher, with versions and an event stream so tools keep working as Horizon changes.
<details><summary>Technical</summary>
`hello` answers with the API version, the release and the features; `horizon-ctl --api`; MCP serverInfo; reply schemas generated into the reference; a `test.` namespace for hooks; transcript compatibility tests; `subscribe` as a stream of events on one connection; Horizon option files by key and saved-server commands. Agent control itself (`horizon-ctl`, both MCP servers, the No AI switch) is done: [docs/agent-control.md](docs/agent-control.md), [docs/api/agent-control.md](docs/api/agent-control.md).
You can port a mod to Horizon with your own AI agent on your own computer. Horizon documents how and gives you the tools, but provides no hosted agents.
<details><summary>Technical</summary>
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide ([website/docs/agents.md](website/docs/agents.md), [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
</details>
### Community
#### Horizon on X (coming soon)
Coming soon: an official Horizon account on X, used to announce releases and the weekly summary.
<details><summary>Technical</summary>
Kira already posts the roadmap items marked `announce = true` when they ship, and the weekly summary, as soon as the account's credentials exist ([roadmap/README.md](roadmap/README.md), 'The journal'). Waiting for the account itself.
</details>
### Code and tools
#### Pipeline protection and a GitHub App for agents
Safer releases: changes to the release machinery need the owner's review, and AI agents use a smaller identity of their own instead of a personal account.
<details><summary>Technical</summary>
`.github/CODEOWNERS` lists the workflows, the release and signing tools, the update-checking code and the installers ([#1015](https://github.com/ludovic111/horizon/issues/1015)); the branch protection that makes it count waits for the `horizon-agent` GitHub App, which reads and writes code, pull requests and issues but can't change settings, workflows, secrets or members, and whose tokens last an hour. It needs the owner to create the App and switch the protection on ([docs/security.md](docs/security.md)).
</details>
#### Signed updates everywhere, installers included
Every download Horizon makes is checked against the project's signature, including the first install with install.sh and install.ps1.
<details><summary>Technical</summary>
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/ludovic111/horizon/issues/1011), [#1014](https://github.com/ludovic111/horizon/issues/1014)). Left: the first install through `install.sh` and `install.ps1` still trusts only the checksum GitHub publishes, because verifying Ed25519 signatures needs OpenSSL 3, which macOS doesn't ship ([docs/security.md](docs/security.md)).
</details>
## Later
### Performance
#### Exact far terrain from real chunks
Far terrain drawn from the world you really built and explored, caves and builds included, not only a preview from the seed.
<details><summary>Technical</summary>
Keep a 3D mip store on disk built from real chunks, drawn by the GPU beside the seed preview for areas nobody has visited. Written from scratch and measured before it is kept. Builds on Far Detail and the far-terrain levels of detail already shipped; the open work on them is [#718](https://github.com/ludovic111/horizon/issues/718).
</details>
### Modpacks
#### Older Fabric and Forge mods
Recognise mods built for versions before 26.1 in the mod browser, so they can be checked and carried over like newer ones.
<details><summary>Technical</summary>
A mapping table from intermediary and SRG names so pre-26.1 jars can be classified, and a rename for mod ids with dashes (`fabric-api`); the port step then treats them like newer jars ([docs/mod-browser.md](docs/mod-browser.md)).
</details>
#### A modpack's own resource and shader packs
Importing a modpack also brings the resource packs and shader packs it ships with, where Horizon can use them.
<details><summary>Technical</summary>
Porting a pack carries data, assets and worldgen today; the resource packs and shader packs bundled with it are left out. Resource packs can be carried as they are; shader packs go through the converter ([docs/modpacks.md](docs/modpacks.md)).
</details>
#### Whole modpacks, such as ATM10, rewritten
Rewriting a complete big modpack for Horizon so it runs far better than the original. Not a priority now: the work above comes first.
<details><summary>Technical</summary>
The project's first goal for modpacks: support them ten times better than vanilla and port any of them. It was set aside in favour of compatibility through the browser. What exists: ATM10's world generates and 42 of its 496 mods are carried at least in part ([docs/modpacks.md](docs/modpacks.md)). It depends on 'Mods with code' and the native mod API.
</details>
#### Running Java mods that aren't rewritten
Run Java mods that nobody has rewritten for Horizon yet, through a bridge or a converter. Hard and not started: the Convert button and native rewrites come first.
<details><summary>Technical</summary>
Earlier research ([#527](https://github.com/ludovic111/horizon/issues/527), closed) found that Java mods can't simply run, because they call Minecraft's own classes. Options to look at again: a jar-to-WebAssembly backend, or a bridge for simple Fabric mods. Whatever is chosen has to run inside the same sandbox and capability checks as native mods.
</details>
### Servers and multiplayer
#### Ely.by and other sign-in providers
Sign in with other account providers, such as Ely.by or any Yggdrasil-compatible one, beside Microsoft.
<details><summary>Technical</summary>
Issue [#1270](https://github.com/ludovic111/horizon/issues/1270) asks for Ely.by and Yggdrasil-compatible servers with an account selector in the launcher. It builds on the Microsoft sign-in ([docs/microsoft-account.md](docs/microsoft-account.md)).
</details>
#### Online-mode checks for Play with Friends
Friends who join through an invite code could prove who they are with their Microsoft account, as on online-mode servers.
<details><summary>Technical</summary>
Play with Friends joins over an encrypted tunnel with an invite code ([docs/play-with-friends.md](docs/play-with-friends.md), 'Identity'). Online-mode checks against Mojang's session servers can be added on top of the same tunnel; they need Microsoft sign-in first, which is coming soon. Play with Friends stays self-hosted between players, with no relay run by the project.
</details>
### Mods and API
#### Orbital Laser on the normal game
The community mod Orbital Laser, made for a fork of Horizon, running on the normal game and listed in the registry as the showcase of the mod API.
<details><summary>Technical</summary>
A pull request on [https://github.com/Stink-O/orbital-laser](https://github.com/Stink-O/orbital-laser) (MIT) once the input, ui, render and world capabilities exist (see 'Screens, input and drawing for mods'): today the mod needs them through its author's fork. Tested in the game, and listed in the registry.
</details>
#### Native versions of popular client mods
Built-in versions of the client mods players ask for: block info on hover, a recipe viewer like JEI, a world map and schematic overlays.
<details><summary>Technical</summary>
Done as extras: a mod list and menu, a minimap and a zoom ([docs/extras.md](docs/extras.md)). Left: block info tooltips, a recipe viewer (a contributor offered to port JEI if it becomes a native feature), a world map and a schematic overlay. They need the read-only interfaces `inventory-read`, `recipes`, `world-read` and `render` in the mod API, or they go into the game directly.
More: [docs/mods.md](docs/mods.md)
</details>
#### Create-style physics as a native mod
Contraptions and physics like the Create and Aeronautics mods, as a mod for Horizon rather than part of the base game.
<details><summary>Technical</summary>
Issue [#805](https://github.com/ludovic111/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod on the mod API. It needs server mods, entities and rendering first.
</details>
#### Proximity voice chat as a native mod
Hear nearby players talk, as with the voice chat mods of other Minecraft versions, as a Horizon mod.
<details><summary>Technical</summary>
Needs server mods, mod networking and an audio interface in the mod API before anything can be built ([docs/api/mods.md](docs/api/mods.md)).
</details>
### Gameplay parity
#### Every Minecraft version, prepared automatically
Every past and future Minecraft version prepared for Horizon automatically. When Mojang releases one, Horizon follows it, and every system is checked against it again.
<details><summary>Technical</summary>
The port pipeline ([docs/version-ports.md](docs/version-ports.md), `tools/version_port.py`) detects a release, prepares it, diffs its classes against the pinned 26.3, regenerates the data-driven tables, runs every parity harness on it and writes a port report that becomes issues. The goal is as little human work as possible for any version, past or future. Detection and the first slices are merged; the scheduled agent that runs it every day isn't installed yet. Two versions in one build are out of scope.
</details>
#### Controllers: tested on real pads, and in the launcher
Make controller support solid: tested with real PlayStation and Xbox pads and a Steam Deck, and usable in the launcher too.
<details><summary>Technical</summary>
The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read with gilrs, menus, rumble, glyphs, remapping and DualSense adaptive triggers, lightbar and haptics, but not yet confirmed on real pads or a Steam Deck (DualSense haptics stay behind a switch until then). Left: a test pass on real hardware, and the same focus navigation in the GPUI launcher.
</details>
#### The full debug screen
Vanilla's complete F3 screen: debug options, a profiler chart, ten-second captures and an atlas dump.
<details><summary>Technical</summary>
Today F3 shows position, chunk, light, biome and FPS ([docs/native-client.md](docs/native-client.md)). Vanilla also has F3+F6 debug options with profiles, F3+1 the profiler pie, F3+L a ten-second capture and F3+S the atlas dump ([#562](https://github.com/ludovic111/horizon/issues/562) asked for the F3 editor).
</details>
### Rendering
#### A light default shader pack
A small built-in look (tone mapping, colour grade, a cheap vignette) that costs almost nothing, for players who want a nicer picture without a heavy pack.
<details><summary>Technical</summary>
An opt-in, composite-only pack on the native shader API, under about 0.3 ms at 1080p, shown on the Shader Packs page. Pack option sliders and profiles, and a Failed row with the full error, belong with it.
More: [docs/shaders.md](docs/shaders.md)
</details>
#### Optional ray tracing
An optional ray-tracing mode for graphics cards that support it (RTX and recent AMD): shadows, water reflections and global lighting on top of the vanilla look.
<details><summary>Technical</summary>
A spike with ray-traced sun shadows, off by default, is a draft ([#1180](https://github.com/ludovic111/horizon/issues/1180)). Reflections, global illumination, Apple M3 and newer if it is cheap, and a performance report per card come after.
</details>
#### HDR monitors
Output for HDR monitors: brighter highlights and richer colour where your screen supports it.
<details><summary>Technical</summary>
Issue [#1441](https://github.com/ludovic111/horizon/issues/1441). Needs a surface format and a tone-mapping path per backend, and tests on real HDR screens on Windows, macOS and Linux.
More graphics options: multisample anti-aliasing, anisotropic filtering and an exclusive fullscreen mode. Today Horizon has FXAA and borderless fullscreen.
<details><summary>Technical</summary>
Not implemented ([docs/graphics.md](docs/graphics.md)). Anisotropic filtering needs linear magnification in wgpu, which blurs close blocks, so close-up sharpness has to be kept first; MSAA would sit beside FXAA; exclusive fullscreen where the backend supports it.
</details>
### Launcher
#### Lean installs that use less disk
Smaller installs: Horizon keeps one shared copy of the prepared game files and removes what only the preparation needed.
<details><summary>Technical</summary>
A shared resource store keyed by the preparer's digest, no prepare intermediates left behind, the current and the previous version kept, and an automatic clean-up. Not started.
</details>
### Platforms
#### Horizon on Linux ARM64
Game and launcher builds for 64-bit ARM Linux computers.
<details><summary>Technical</summary>
Issue [#1709](https://github.com/ludovic111/horizon/issues/1709): aarch64 builds of the game and the launcher in the release workflow, tested on a real ARM machine with its Vulkan driver. The server build already drops audio and controllers (`--no-default-features`).
</details>
#### OpenGL for old computers
A way to play on old PCs and Linux machines that have no Vulkan or DirectX 12, through OpenGL.
<details><summary>Technical</summary>
Players on older hardware ask whether Vulkan is the only way. Horizon draws with wgpu, which has an OpenGL backend; what is needed is to see which features hold up on it, how fast it runs, and a launcher switch like the DirectX 12 fallback that already ships.
More: [docs/graphics.md](docs/graphics.md)
</details>
#### Android, iOS and the browser
Versions of Horizon for Android, iOS and web browsers. Nothing official yet; contributors have started the first pieces.
<details><summary>Technical</summary>
No official build. Android was first closed as not planned, with touch controls, a mobile interface, packaging and loading the player's own assets all missing; the roadmap now includes it, iOS and a WebAssembly build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/ludovic111/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/ludovic111/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/ludovic111/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
</details>
### Horizon extras
#### A complete video studio inside the game
Record, replay and edit videos without leaving the game: record in a keypress, replay a session, cut and finish the video. One of Horizon's original goals.
<details><summary>Technical</summary>
Three stages. Recording: capture gameplay in a keypress with an encoder thread and little frame-time cost. Replay: a replay mode for recorded sessions. Editing: cuts, camera paths, titles and export. None of it exists yet; the agent-control socket ([docs/agent-control.md](docs/agent-control.md)) already lets tools drive the game for scripted shots.
</details>
### AI and agents
#### An optional AI teammate
An optional AI teammate you can talk to by voice. It plays beside you, helps or finishes the game with you, runs on your computer first, and exists only if you turn it on. The No AI switch hides it.
<details><summary>Technical</summary>
Draft pull requests: the companion ([#1068](https://github.com/ludovic111/horizon/issues/1068)), its safety modes and their evaluation ([#1069](https://github.com/ludovic111/horizon/issues/1069)) and a route that finishes the game ([#1070](https://github.com/ludovic111/horizon/issues/1070)). In the drafts: a companion that follows, fights, mines, crafts, sleeps and respawns; chat and push-to-talk voice, with speech recognition and a choice of voices; presets (very strong, helper, beat the game); a guard model with Clean, Kid and Salty modes; perception levels from Fair to All-knowing; hardware detection with a simple mode and an Advanced tab; local models first, cloud keys optional. The No AI switch is already in the game. Left: the speedrun route doesn't finish the game reliably yet, voices for more languages, and the safety evaluation, which must pass before it ships.
</details>
### Community
#### A video series about Horizon
A series of videos about how Horizon is made and what is new, with each upload announced on Discord.
<details><summary>Technical</summary>
Kira already shows new uploads in a read-only Discord channel (done). The series follows each stage of the project: a script, edits, titles and a thumbnail per video.
</details>
## Help wanted
### Performance
#### Benchmark your computer
Run Horizon's built-in benchmark and share the report: every new computer makes the performance work more accurate. No coding needed.
<details><summary>Technical</summary>
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it ([website/docs/performance.md](website/docs/performance.md)). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
</details>
### Gameplay parity
#### End city banners keep their pattern
Banners in End cities show as plain magenta instead of with their pattern.
<details><summary>Technical</summary>
Compare the banner block entities the End city templates place with vanilla's (`EndCityPieces`, the templates' block entity NBT, `BannerBlockEntity`'s patterns), in `crates/horizon-world/src/worldgen/structure/`. Issue [#1438](https://github.com/ludovic111/horizon/issues/1438); possibly related: [#526](https://github.com/ludovic111/horizon/issues/526).
</details>
#### Enchanted loot in End cities
Armour and tools in End city chests should come enchanted, as in vanilla.
<details><summary>Technical</summary>
`loot.rs` implements `enchant_with_levels`: find why the End city chest table's rolls come out bare, and compare them with vanilla's for a few seeds. Issues [#1437](https://github.com/ludovic111/horizon/issues/1437), [#1660](https://github.com/ludovic111/horizon/issues/1660).
</details>
#### Cooldowns drawn on items
After you throw an ender pearl, its slot should show the cooldown sweeping away, as in vanilla.
<details><summary>Technical</summary>
Vanilla draws `ItemCooldowns`' percent as a white overlay over the slot (`GuiGraphicsExtractor.itemDecorations` and `itemCooldown`) in the hotbar and in inventories. Issue [#1763](https://github.com/ludovic111/horizon/issues/1763).
</details>
#### Chat settings and team prefixes
The Hidden and Commands Only chat settings should hide chat, and team prefixes should appear before names in chat.
<details><summary>Technical</summary>
Vanilla checks `ChatVisiblity` before it delivers chat and system messages ([#1376](https://github.com/ludovic111/horizon/issues/1376)), and chat uses the team-formatted display name ([#1378](https://github.com/ludovic111/horizon/issues/1378)). Both issues have exact steps to reproduce.
</details>
#### A pointing hand over buttons
The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3.
<details><summary>Technical</summary>
26.3's `AbstractWidget` asks for a cursor from `CursorTypes` when it is hovered; winit's `CursorIcon::Pointer` gives the same shape. Issue [#1347](https://github.com/ludovic111/horizon/issues/1347).
</details>
#### Ctrl+Backspace and scroll bars in menus
Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable.
<details><summary>Technical</summary>
`EditBox` deletes by word with Ctrl ([#1764](https://github.com/ludovic111/horizon/issues/1764)); the Game Rules and Statistics lists' scroll bars can't be dragged ([#1782](https://github.com/ludovic111/horizon/issues/1782), after [#978](https://github.com/ludovic111/horizon/issues/978)).
</details>
#### Chickens flap while they fall
Chickens should flap their wings while they fall, not stand still in the air.
<details><summary>Technical</summary>
Vanilla's `Chicken.aiStep` updates `flap` and `flapSpeed` when the chicken isn't on the ground, and the model's wings follow them. Issue [#1706](https://github.com/ludovic111/horizon/issues/1706).
</details>
## Recently shipped
- **Server resource packs** (v0.2.48): Servers can send you their resource pack: Horizon asks, downloads it and applies it, like vanilla. [#1562](https://github.com/ludovic111/horizon/issues/1562) · [#1569](https://github.com/ludovic111/horizon/issues/1569) · [#1570](https://github.com/ludovic111/horizon/issues/1570)
- **Single player on its own server thread** (v0.2.39): Single player runs on a server thread, as vanilla does, so the world keeps ticking smoothly while the window draws. [#1514](https://github.com/ludovic111/horizon/issues/1514) · [docs/server-levels.md](docs/server-levels.md)
- **GPU occlusion culling on Macs** (v0.2.17): On Macs the GPU now skips terrain hidden behind hills and caves: 8 to 48% more frames per second at 32 chunks on an M4 Pro. [#1434](https://github.com/ludovic111/horizon/issues/1434)
- **Animated textures on the GPU** (v0.2.5): Water, lava and other animated blocks are now animated by the graphics card: 1.7 to 2.4 times better 1% lows on Intel graphics. [#1239](https://github.com/ludovic111/horizon/issues/1239)
- **Far Detail for integrated graphics** (v0.2.5): An optional Reduced setting for far terrain, suggested on integrated graphics, that saves 2 to 4 ms of GPU time per frame. [#1199](https://github.com/ludovic111/horizon/issues/1199) · [#1029](https://github.com/ludovic111/horizon/issues/1029)
- **The launcher's mod browser** (launcher@0.2.0): Search Modrinth for mods, modpacks and resource packs from the launcher; it says before installing what works in Horizon. [#1154](https://github.com/ludovic111/horizon/issues/1154) · [#1242](https://github.com/ludovic111/horizon/issues/1242) · [#1243](https://github.com/ludovic111/horizon/issues/1243) · [#1246](https://github.com/ludovic111/horizon/issues/1246) · [docs/mod-browser.md](docs/mod-browser.md)
- **A blocky, Minecraft-like launcher** (launcher@0.2.0): The launcher got a new blocky look in Horizon's purple, in dark and light. [#1269](https://github.com/ludovic111/horizon/issues/1269) · [docs/design-system.md](docs/design-system.md)
- **Dynamic lights** (v2026.10.22): An optional extra: torches you hold, drop or set on fire light up what is around them. [#1162](https://github.com/ludovic111/horizon/issues/1162)
- **Host a server from the launcher** (launcher-v0.15.0): The launcher's Servers page creates, starts, backs up and updates a Horizon server with buttons, and invites friends. [#1026](https://github.com/ludovic111/horizon/issues/1026) · [docs/hosting.md](docs/hosting.md)
Loaded 100 of 1374 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.