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]>
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]>
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 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]>
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]>
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]>
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]>
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]>
- `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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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.
🤖 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 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. Ludovic asked on 2026-10-23 for every hardware fact players send (opt-in). The end-to-end run against a local server is in the description: breakdowns need ≥3 installs, with no install ID or exact value. The consent version goes up to 2, so players who opted in are asked again, as the privacy page promises.
🤖 Agent: merged after verify passed and a local run on the Mac mini. lan_e2e on the server thread passes for host and guest: boats, minecarts, frames, ladder, lightning, riding and mounts. Parity --changed passed 25 of 25, and the server-thread default is confirmed. Single player runs on its server thread again. Thanks @rdbtCVS for pushing for the root fix.
Under Wayland the title screen can come before the window is on a screen, so the
report's hardware.display stayed empty (seen end to end in a headless sway).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The desktop measured: Horizon v2026.10.22.13 against vanilla 26.3 at render
distance 12 and 32, 3 runs each, 1280x720, VSync off, in a headless sway on
the RTX 4080 (never on its screen). Replaces the "not yet measured" note.
- /benchmarks shows each players' group's hardware split (RAM, VRAM, drive,
system, GPU kind) and, once the telemetry server has some, a "Players'
hardware" table from /v1/aggregate's breakdowns. Hidden without data.
- The picker takes the memory too. Players' groups whose installs have neither
the picked RAM nor the GPU's VRAM class count as further away, and the result
says what they had and when a game's peak memory is a large share of the RAM.
- Each GPU in the spec table has its memory (0: shared).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Right after a window is made, Wayland doesn't say which screen it is on, so
current_monitor() is None and display was missing (seen end to end in a headless
sway). Fall back to the primary, else the only, monitor, and read it again when a
session ends; agent control's status (the benchmark report's display) uses the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Settings gets an AI group with the No AI switch (horizon_core::no_ai, the
same switch the game reads) and, while it is off, a Connect an AI agent
section that replaces the old command-line note: a picker for Claude Code,
Codex, Cursor or any MCP client, the ready-made config with a Copy button,
the client's own add command (claude mcp add / codex mcp add) shown and run
only when the player clicks Add, and Test the connection, which starts the
MCP server as a client would and says how many tools answered. Client CLIs
are found on the PATH or in the folders installers use, since a launcher
started from the desktop gets a short PATH on macOS.
horizon-launcher agent-config --test runs the same test; the launcher
harness checks it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The MCP tools are generated from the CLI's clap definition (cli_tools.rs):
mods_search is horizon-launcher mods search, with the same arguments, and
a call runs that command with --json. So the mod browser, modpacks, hosted
servers, settings, game arguments, data collection, desktop entry and the
benchmark are all reachable, and new CLI commands become tools by
themselves. Destructive ones need confirm: true and carry destructiveHint.
The session keeps play, server (with server_command/log/stop),
account_list and account_sign_in in-process, renamed after the CLI
(launch_client is play; play takes join for an invite code).
New CLI commands: settings [KEY VALUE] (selected version, folders,
install-new-versions, opening animation) and agent-config, the MCP config
for Claude Code, Codex, Cursor and any client. play takes --world and
--creative. Under No AI, mcp and agent-config are hidden and refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The table gains what only the raw socket could do (options pages, connect
and invite codes, disconnect, LAN, Play with Friends, books, signs, mount
inventories, effects, xp, enchant, potion, fill, containers) and new
commands: worlds, world-rename, world-backup, world-delete --confirm (the
Select and Edit World screens, from the title screen only), servers (the
Multiplayer list) and interact-entity (right-click an entity). status,
mouse, pad, release, bench and stats become MCP tools; summon takes a
position. Destructive and network tools carry MCP annotations.
docs/horizon-ctl-commands.md is generated from the table
(horizon-ctl help --markdown) and a test fails when it drifts. The
agent_control_e2e manage scenario exercises the new commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_core::no_ai reads and writes the noAi line of the game's
horizon-extras.txt, so the launcher and the game share one switch. While
it is on the game never opens its control socket, horizon-ctl refuses every
command with exit 4 and kind denied, horizon-ctl mcp and horizon-launcher
mcp refuse to start (and refuse calls if it is turned on mid-session), and
horizon-launcher no-ai [on|off] shows or sets it. The game's extras keep
the key when they rewrite the file. Both e2e harnesses check it.
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]>
🤖 Agent: merged after verify passed. Go-day step 5 (docs/versioning.md): Ludovic said go on 2026-10-23 ("tu peux envoyer la 0.2"). The bridges are out (v2026.10.22 and launcher-v0.17.0), and the latest stable is v2026.10.22.07, built after the freeze. The launcher redesign (#1269) and the mod browser are merged.
🤖 Agent: merged after verify passed. Ludovic approved the redesign on 2026-10-23. Integration run on the desktop by integ-w3 (main + the mod browser stack + this), then main merged and re-checked on the Mac mini (launcher clippy, tests).
What players send (opt-in, unchanged switches) now describes the whole machine:
CPU cores and base/boost clock, the GPU's maker, integrated or discrete and its
VRAM, the drive under the game folder (NVMe/SSD/HDD), the screen (size, refresh,
scale), on battery or mains, laptop or desktop. Perf, crash and bug items carry
them in `system`; benchmark reports in `hardware`. No serials, host or user
names, or paths. CONSENT_VERSION goes to 2, so players are asked again.
The server keeps the new fields in `systems.extra` (the table is rebuilt once
on old databases, ids kept). /v1/aggregate and /v1/benchmarks/summary add
breakdowns by RAM class, VRAM class, drive, OS and integrated/discrete GPU;
a class is published only with at least 3 different installs, never a raw value.
website/scripts/benchmarks.mjs mirrors the classes on the shared fixtures.
The summary's "not Horizon or plain vanilla" check returned None (accepted)
instead of the rejection; it now matches the site's script.
The launcher's "View what would be sent" and `data show` list every field.
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]>
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
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]>
A server's players (single player on its server thread, LAN guests,
dedicated-server players) get the horse inventory: shift + use on a tamed
horse, donkey, mule, camel or llama, or the inventory key while riding one
(ServerboundPlayerCommandPacket OPEN_INVENTORY), opens HorseInventoryMenu
over the mount with ClientboundMountScreenOpenPacket. Clicks run on the
menu engine's new Kind::Mount (saddle and body ArmorSlots that take what
the mount may wear, the chest, AbstractMountInventoryMenu.quickMoveStack),
and the slots go back to the mount with the item's equip sound.
The client shows a server's mount screen, predicts its clicks with the
same engine and sends OPEN_INVENTORY when the inventory key is pressed
while riding. Llamas now have their inventory in the window too: the
carpet slot and a chest of `strength` columns, no saddle slot.
lan_e2e `mounts` checks it for the host and a guest, with and without
--server-thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The home page's "At a glance" list gets a fifth row with how many play right now, read from
https://telemetry.playhorizon.dev/v1/live (3 s timeout, again every minute while the tab is
visible). It stays hidden while nobody plays or the server doesn't answer. The FAQ no longer
says the "I'm playing" ping is off: the server is live since #1276.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. telemetry.playhorizon.dev is live on Railway (service telemetry in horizon-site, volume /data): /healthz, /v1/live (a test ping counted) and /v1/benchmarks/summary (CORS for playhorizon.dev) answer. The PR's local end-to-end runs are in the description.
launcher/src/main.rs: main (#1271) removed the Game menus setting (one menu
style now) where this branch restyled Settings. Kept the Game and About
group headings, dropped the Game menus row as main does, and renumbered the
rows after it (game arguments n+4, Horizon n+5, Launcher n+6).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. Local proof on the merged head bf274d92: fmt, clippy -D warnings, client and launcher tests, parity --changed origin/main, and test_friends.py --server-thread. CI parity passed on the earlier head. Every Horizon setting is reachable from vanilla-style screens (list in the PR body).
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.
lan_e2e takes main's version: #1280 kept `boats` (passing with and
without --server-thread) and added `minecarts`, `frames` and `lightning`
next to `ladder`, and main's run() already sets HORIZON_SERVER_THREAD
explicitly as this branch does. The code keeps this branch's change:
single player runs on its server thread by default again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player on its server thread, LAN guests and dedicated-server
players could not ride: a server player's turn had no mount input, and
its client had nothing to sit on.
The server now drives a server player's animal from that player's
PlayerInput (forward, back, strafe, jump, sneak, sprint) and rotation,
through the same tick_mount_riding the window's player uses:
- the jump bar charges per player (MountState swapped with the turn);
- sneaking gets off, with SetPassengers and the teleport beside it;
- a mount ridden by another player can't be taken or fed (isVehicle);
- an animal whose rider left stops taking its input.
The client sits where positionRider puts it on the animal it was sent,
and charges its own jump bar for the HUD.
Ridden movement (pre-existing, also in the window): Mob.setSpeed sets zza
too, so the ridden input set before the speed was lost and pigs and
striders crawled while horses walked on with no key held; the input is
now set after the speed (travelRidden). The ridden speed uses the
effective movement speed (a cold strider's modifier, potions), and a
shivering strider rides at 0.35 of it (Strider.getRiddenSpeed).
lan_e2e riding: the host and a guest saddle and ride a horse (walk and
jump), a pig and a strider with their sticks, a camel and a donkey, sit
on a llama, harness and fly a happy ghast, and get off, with and
without --server-thread. `mobs` reports ridden, controlled, saddled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
With #1285 Play with Friends works on the server thread: friends_view
offers sharing there, so the pause menu's Friends icon is active on both
paths and inactive only on another server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged on verify plus local proof. tools/test_friends.py passes with the host on its server thread and in the frame (code, self-hosted relay and short code, stop). Parity --changed: 22/0. Whichever of #1271 and this lands second drops #1271's server-thread greying of the Friends icon.
🤖 Agent: merged on verify plus local proof, since CI parity is queued behind the Mac mini runners. Parity --changed origin/main on this head: 102/0. lan_e2e passes with and without --server-thread, for the host and for a LAN guest: boats, minecarts, frames, ladder, lightning. The default is unchanged here; #1265 follows once riding animals work for a server's players too.
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]>
Conflict: #1237's weather now returns its own vertices; the title draws
none (the live title backdrop is gone).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
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]>
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
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]>
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v7: core 99 and worldgen 81 tests, 23 worldgen/density/noise parity harnesses 23/0; bit-exact by construction, with a test against the scalar path).
Sharing a world with an invite code refused with the integrated server
on its own thread ("open a singleplayer world first"): the window is a
client there. It now shares that server as Open to LAN publishes it:
friends_start publishes it when it isn't, the invite tunnel points at
its port, stopping sharing unpublishes it again, and the pause menu
shows the Friends entry with the players the server lists.
test_friends.py --server-thread runs the host's world on its server
thread; the host's own move and blocks go through /tp, /fill and
/setblock, which the server answers in both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
"Horizon 2026.10.22.07: #1255, mob pushes, fix, #1222" now shows "Single player runs in the frame again
until server players have vehicles and 3 more".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- ThinkPad (i7-7500U, HD Graphics 620, Mesa): Horizon v2026.10.22.04 (two flights per scene) against vanilla
26.3 (two games) at render distance 12. Java loads chunks faster there and flies the mountain faster; the
page says so. Render distance 32 isn't published for it: neither game loaded the spawn in 5 minutes, and
Horizon kept only 900 to 1,400 columns resident when given 25.
- "Where Java is ahead" also lists scenes where vanilla's average beats Horizon's by more than 5%.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: the menu blur (#1249) now runs without the Horizon style check,
chat and the tab list draw under every screen as before the blur split,
and the HUD's second range draws without the removed colour grade. Play
with Friends blurs the world behind it like vanilla's other menus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#1234 replaced column_size with a LazyLock table and #1238 added the
lightning quad count to gfx.rs's max chain; this branch rewrote both
places. weather.rs keeps this branch's code with #1234's table and tests;
gfx.rs takes the max over cloud, lightning and weather quads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With single player on its server thread the window drew no lightning,
and LAN guests and dedicated-server players never did: the server never
sent the bolt (it lived outside the tracked entities) and the client had
nothing to draw for a lightning_bolt AddEntity.
The server gives each bolt an entity id and tracks it like the other
entities; the client adds a visual-only bolt for it (LightningBolt.tick
off the server: sounds, the sky flash, the drawn bolt) and ticks bolts
and the flash in its level tick. `status` reports bolts and flashes, and
lan_e2e's `lightning` check summons one beside each player and asserts
both sides get the bolt and the flash.
lan_e2e: the vehicle checks put the players apart first, and run()
sets HORIZON_SERVER_THREAD explicitly either way, as #1265 does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player on its server thread, LAN guests and dedicated-server
players could not board, drive or break boats and minecarts, nor use
item frames and armor stands: the client sent Interact/Attack only for
mobs, the server aimed only at mobs, and nothing carried riding over
the network.
Client (remote.rs, remote_vehicles.rs):
- Interact/Attack for boats, minecarts, item frames, armor stands and
paintings, with the hit location (Minecraft.hitResult).
- SetPassengers seats the player; a boat ticks on the rider's client and
goes to the server as MoveVehicle and PaddleBoat every tick; a
MoveVehicle back puts it where the server says; a minecart moves on the
server and the rider sits in it.
Server (lan_vehicles.rs, lan_actions.rs, hub.rs):
- look_at_entity aims at every pickable object, at the client's hit point.
- The guest's vehicle is its Entity.vehicle during its turn; tick_boats
and tick_carts leave guest-ridden vehicles to their riders.
- handleMoveVehicle with its checks (invalid values kick, too quickly,
moved wrongly, colliding with anything new), handlePaddleBoat.
- Player.rideTick: sneaking gets out, then SetPassengers and the
teleport beside the vehicle (dismountTo); a minecart moves with the
rider's input; a passenger's moves only turn it.
- SetPassengers for guests through hub.riding; one rider per vehicle.
lan_e2e: boats (host and guest board, paddle, the other side sees the
boat move, sneak out, break), minecarts and frames (item frame and armor
stand) for both, passing with and without --server-thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
#1264 wrapped build_surface in `if !self.identity_end_surface(pos)` where
this branch adds the uniform_biomes field to the same SurfaceQueries. Kept
the condition with the new field inside it; both tests are kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
Without min_chunks (the old call) wait_loaded behaves as before, which tests/test_perf_bench.py's
old-builds case relies on.
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]>
- DEFAULT_ENDPOINT is https://telemetry.playhorizon.dev. The heartbeat, the launcher's live count and the
outbox upload all go there; HORIZON_TELEMETRY_URL still names another server, and HORIZON_TELEMETRY_URL=off
sends nothing at all. CI workflows and the harnesses that start the game or the launcher set off. Consent
is unchanged: the four switches stay opt-in, only the anonymous ping is always on.
- GET /v1/benchmarks/summary: players' benchmark uploads grouped by GPU, CPU, render distance and game, groups
of 3+ installs only, medians per install then across installs, no install ID, day, OS version, driver,
memory or screen. Cached 10 minutes, CORS for the website. Same rules as website/scripts/benchmarks.mjs,
tested on the same fixtures; a deleted install leaves its groups at once.
- /v1/aggregate no longer lists the newest 50 raw benchmark reports (hardware, driver, day, one install each):
uploads are published only through the summary.
- Behind Railway's proxy the rate limit counts X-Real-IP, else the last X-Forwarded-For entry (the one the
proxy adds; a client could choose the first). Still in memory only, never stored or logged.
- crates/horizon-telemetry-server/railway.json: Dockerfile build from the repository root, health check
/healthz, one replica in europe-west4 (one SQLite file on the /data volume).
- Docs: docs/telemetry.md, the server README (the summary's shape, Railway setup), the privacy page (the ping
is on, where the data lives, how benchmark uploads are shown).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Right after a teleport, and at spawn, Horizon v2026.10.22.04 reports nothing queued and steady chunk counts
for over a second before it starts loading. wait_loaded() took that for "loaded" and stopped the clock with
0 to 3 sections meshed (load 0.1 s), so the flights ran over empty terrain and the frame rates meant nothing.
Now the counts must change once since the wait began (or stay at the full view for 5 s), and the resident
chunks must reach 80% of the view circle (pi x RD^2), before a steady second counts as loaded.
Measured with it on the Mac mini and the ThinkPad for the website's Benchmarks page (#1274). At RD 32 a 4-thread
laptop needs --timeout above the default 300 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three runs of each game at render distance 12 and 32 (tools/perf_bench.py, seed 1234, 1280x720, VSync off).
Horizon's average sits at the 144 Hz display (macOS presents in step with it even with VSync off): marked as
capped on the page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- /releases: every game and launcher release on one page, newest first, Stable / Alpha / Launcher marked and
filterable. The newest three are open; each shows what it added and fixed, the full notes, its downloads and
a link to GitHub. Baked at build time, refreshed from GitHub when a newer release is out. /releases#release-<tag>
opens one.
- Home: "Recently added" replaces the patch notes block: the last five releases on one line each and three
highlights from what they added.
- /benchmarks: Horizon against vanilla Java Edition 26.3 measured on our machines (average FPS, 1% lows, p95
frame time, start to world, chunk loads, memory, at render distance 12 and 32), saying where Java is ahead;
a GPU + CPU picker that shows a measurement when there is one and otherwise a marked estimate with a range,
from our own spec table (method explained on the page); players' results read live from the telemetry
server's GET /v1/benchmarks/summary, falling back to the baked public/benchmarks.json, groups of 3+ installs
only.
- scripts/benchmarks.mjs builds public/benchmarks.json from our reports (and, for tests, telemetry items) with
the same rules as the server; fixtures shared with the server's test.
- Releases and Benchmarks in the nav and footer; docs/performance.md links the page.
- Pixelify Sans' "fi" ligature drew "fixes" as "Axes": common ligatures off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Captured on the Mac mini with the game's offscreen capture (1280x720),
replacing the Horizon menu style's Video page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pause menu's Friends icon is recorded as a widget, so a controller
reaches it and the control socket names it. The sharing screen's long help
URL ran off the screen: a Troubleshooting... button beside Done opens it,
and the hint drops out when it would reach the footer. The graphics docs
say what the two Horizon screens hold (vanilla's own settings stay on Video
Settings).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drop the Horizon menu style (options_horizon.rs, front.rs, the live title
backdrop and its post grade, the launcher's menu-style setting, CLI and MCP
tool). Every screen is vanilla's. Horizon's own settings hang off vanilla's
Options the way mods add theirs: Horizon Settings... and Mods... take the
Telemetry/Credits slots, and Horizon Settings opens Graphics, Advanced
Graphics, Shader Packs, Controller Settings, Extras and Mods, each a vanilla
options list with tooltips. Play with friends moves behind vanilla's Friends
buttons: the pause menu's opens Play with Friends (share, copy, stop), the
title screen's opens Join a Friend, and Direct Connection takes a full code.
Old files load as before: horizon.txt's menuStyle line and
horizon-graphics.txt's level line are ignored.
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 game keeps the offline name as "name:Player12345" in horizon-player.txt;
the top bar and the friends-only server list showed the whole line. Both
now read the name after the key.
With no version installed, Home has no landscape: its title starts below
the top bar like the other pages, and the sidebar says "No version
installed" instead of "Needs preparing".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's warmed-timing change and this branch both edited the usage comment
at the top of examples/chunkgen_bench.rs. Kept this branch's line, which
adds [--snapshot PATH]; no code change.
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]>
Revert commit cdaf1d79a0, preserving the ladder regression check added afterward.
Server-player bugs also affect dedicated servers and Open to LAN. Running single player in the frame only hides those bugs there; keep the shared server-thread path as the default and fix the underlying issues.
🤖 Agent: merged on verify plus local harness proof (CI parity is queued behind about 15 runs). The new lan_e2e ladder check: on main, ten blocks of ladder took the guest from 20 to 13 health; with the fix it lands at full health. Main was merged in, keeping both the ladder and boats checks.
Versions and Patch notes list a hundred releases, and every frame laid out
and painted all of them. Rows more than a screen away from the viewport are
now empty blocks as tall as when they were last drawn (cull.rs), so the
page keeps its length; a row being installed or showing an error is always
drawn. Scrolling Versions on the Mac mini: median frame 8.2 -> 7.0 ms.
HORIZON_LAUNCHER_PROJECT=<slug> opens a project's page on the Mods page,
for screenshots, as HORIZON_LAUNCHER_PAGE opens a page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged on verify plus local harness proof. It is a release-blocker fix, and CI parity is queued behind about 15 runs on the Mac mini. lan_e2e boats fails on main with --server-thread (riding None) and passes with this PR's default (in the frame, the path that was the default until #1114). fmt and clippy -D warnings pass. Server players' vehicles are tracked in FEATURES.md 4x.
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
Blocks palettes (dark default, light, or the system's, chosen in Settings), Pixelify Sans for
headings and controls, Pixelarticons icons, square panels with an outline, a bevel and a lip,
inventory tabs, and pixel art drawn in code: a stone texture behind the pages and a landscape
behind Home. A top bar with the pages and the account chip, and a sidebar with the instances
(Horizon shows the grass block the mod browser draws from the player's prepared textures), the
hosted servers, and the launcher's version and channel. The mod browser's pages (kinds as tabs,
project icons in slots, verdicts as badges, the project page) are restyled, not rebuilt. The
benchmark also measures scrolling the Versions list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
#1200 and the teams change appended test modules at the same place as this
branch's push_tests. Main's modules are kept as they are and push_tests
follows them unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
Player's MoveSimulationType is AUTHORITATIVE_SIDE_AND_SERVER, so a
ServerPlayer travels on the server too, and every tick on a ladder or
vine LivingEntity.handleOnClimbable resets its fall distance. Horizon's
server only replayed the client's moves (ServerPlayer.doCheckFallDamage),
so the fall distance grew all the way down a ladder and hurt the player
where it landed: LAN guests, dedicated-server players and, while single
player ran on the server thread (#1114), the single player.
lan_e2e ladder: the guest slides down ten blocks of ladder and lands with
full health (before: 20 -> 13).
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]>
horizon-theme gains the Blocks palettes (light and dark, WCAG AA on every panel and on the stone),
Pixelify Sans (OFL) and Pixelarticons (MIT); tokens.css now carries the Blocks colours, the pixel
font and the block edges. The site sets headings, navigation and buttons in Pixelify Sans, turns
the index lists into stacks of panels with an outline, a bevel and a lip, puts a stone texture
behind the page and Horizon's pixel landscape (a dawn or a sunset) behind the hero, both drawn in
code by scripts/art.mjs. Light and dark still follow the system, with the toggle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #1114 single player's window is a player of its own server thread,
and a server's players can't use boats or minecarts: the client sends
Attack and Interact only for mobs (remote_start_attack, remote_use_entity),
the server aims a player's attack or use only at mobs and dragon parts
(look_at_entity), and neither side handles riding (no SetPassengers on the
client, no MoveVehicle or PaddleBoat packets). So a boat on water could not
be boarded or broken in single player.
The server thread is opt-in again (--server-thread or
HORIZON_SERVER_THREAD=1); --no-server-thread still works.
lan_e2e boats: the host right-clicks a boat on water and rides it, sneaks
out, and breaks it. Without --server-thread the host now runs the game's
default instead of forcing the frame, so the check covers what players get.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pack lists them with the mods left off, the port's log and fetch label say
the same, and docs/modpacks.md no longer asks for a key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Importing a modpack from the Mods page (or `mods install`) now ports it right away
for the selected version, so Play works straight after. port-pack leaves a
namespace two carried mods both claim to none of them (YUNG's mods all ship
`yungsapi`, which made the game drop three of them), and the launcher applies
the game's first-wins rule before it starts a pack, listing what it left off.
Measured on Vanilla Perfected, Slimes Adventure and All the Mods 10 (docs/modpacks.md):
2, 20 and 42 mods ported, every world loads, no crash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mod_browser_harness.py searches, checks and installs a mod through the launcher's
mods CLI in a scratch game folder, opens a world with it offscreen, places one of
its blocks and crafts one of its recipes on a crafting table, and times the world
load with and without it. Macaw's Fences: check 4.0 s, install 3.1 s, world load
5.4 s -> 7.3 s, hedge placed, 4 crafted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A result's title opens its page in the launcher: icon, authors, downloads,
followers, licence, verdict (for a Java mod, what ports and what doesn't) and
Install, links, then Description (Markdown), Gallery and Versions tabs
(browser::details, Source::details on Modrinth's project and members).
Filters: sort (relevance, downloads, follows, newest, updated), loader, game
version, environment and categories, from the source's tag lists.
Icons and gallery images are fetched at run time over HTTPS from the sources'
image CDNs only, size-capped, decoded and scaled to PNGs in a 128 MB disk cache
(browser::images); a placeholder shows until they arrive. Vanilla's mark is a
grass block drawn from the player's prepared game files. Modrinth's logo isn't
shown: its branding is all rights reserved without written permission.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic dropped CurseForge for now (its API needs a paid key). The Mods page shows
"CurseForge (coming soon)" beside the Modrinth switch, disabled and inert;
browser::sources() is Modrinth only, so no search or install goes to CurseForge,
and `--source curseforge` says it is coming soon. Text that asked for a key or
an environment variable now says CurseForge support is coming soon. Importing a
CurseForge zip or instance still works; jars only CurseForge serves are listed
as "CurseForge downloads coming soon".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rust 1.99's clippy denies chunks_exact with a constant size
(chunks_exact_to_as_chunks), so `cargo clippy --all-targets -D warnings`
failed on this test. Same check, written as as_chunks::<4>().
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game and its integrated server map items and blocks to network ids through
the registries report (registries.json), which the content root left vanilla:
a mod's item in a slot, given or picked up, reached the client as air. The
content root now rewrites registries.json too, appending the new items and
blocks after vanilla's ids in plan order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Mods page gets a Mods tab: Fabric, NeoForge, Forge and Quilt mods for 26.3
(1.21 and later with Show everything). Each result's jar is downloaded (allowed
CDNs, checksum), checked with horizon-port-pack (Works, Partly, Doesn't run, with
what ports and what doesn't) and the answer cached per file. Install copies the
carry into horizon-mods/<id>/ and turns it on in horizon-mods.txt, with required
mods that port; libraries with nothing to port are listed as not ported; id or
namespace clashes are refused. Remove deletes it and turns it off.
The Modpacks page becomes the Mods page's Modpacks tab: imported packs first,
then the search. CLI: mods search --kind mod, mods check <project>.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A recipe whose result is one of the mod's own items that the carry didn't declare
(its block had no vanilla template) loaded and crafted air. Vanilla refuses a
recipe for an unknown item; the carry now moves such recipes to outside/ and lists
them (recipes_unregistered), and check says "N recipes for left-out blocks".
Found by the mod browser's end-to-end run on Macaw's Fences.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`horizon-port-pack check <jar> <out>` carries a single jar into a folder content
mod and reads its class files (constant pool, superclass, declared methods) for
what the carry can't take: block entities, menus, entities, blocks and items with
behaviour of their own, recipe types, worldgen code, Mixins, game events. The
verdict and the lists of what ports and what is skipped go to check.json, for the
launcher's mod browser to show before install.
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]>
Review fixes on top of the particle serializers:
- PARTICLES (17) is List<ParticleOptions>: a VarInt count, then each
particle. Reading it as one particle only worked for empty lists, so a
living entity with effect particles (LivingEntity.DATA_EFFECT_PARTICLES)
misread everything after it. Value::Particles holds the list.
- Item particles use ItemStackTemplate.STREAM_CODEC (item, count, patch),
not the optional slot codec.
- Vibration keeps its position source (block position or entity id and
y offset) so it writes back the bytes it read; geyser data keeps its
Int exactly instead of going through f32.
- Enchantment names come from the server's synced registry, not the
builtin report (enchantment is data-driven).
- Armor stands: 26.3's DEFAULT_DEPTH_CLEAR_VALUE is 0.0 (bytecode
dconst_0), so a marker's empty box falls back to a range of 4 and a
visible marker stand draws; restore that. Holograms stay hidden through
the invisible flag.
Test: particles_round_trip covers every data shape, a PARTICLES list and
the entry after it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. The fast channel is now "Alpha" (Ludovic, 2026-10-22); old "nightly" / beta_builds settings still read as Alpha. verify passes; launcher tests 96+14+1, release tools' Python tests 27, website build OK.
The whole of Horizon is in beta, so the channel that installs every build as
soon as it is built is called Alpha; Stable keeps its name. The launcher's
Settings, the update-channel command, the MCP tool, the release notes footer
and the docs say Alpha. update_channel is saved as "alpha"; "nightly" and the
old beta_builds switch still read as Alpha.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Mods page (launcher 0.17) replaces "a mod browser is planned"; the Horizon
shader format is no longer called planned; Nightly-only things say Nightly.
The "I'm playing" ping has no server (DEFAULT_ENDPOINT is None), so the FAQ and
the privacy page say it is off instead of sent. The channel switch says it
needs the launcher release after 0.17. The README says what deploys the site.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
With a JDK installed, the launcher passed the preparation the home of the
JDK horizon-prepare finds (javac/../..) as JAVA_HOME and on the PATH.
Found through the PATH, that is often a stub: on macOS /usr/bin/javac
gives JAVA_HOME=/usr, and `JAVA_HOME=/usr /usr/bin/javac -version` never
returns (the stub calls itself), so the preparer subprocess hung on a Mac
with Temurin installed. When horizon-prepare finds a JDK by itself, run
the preparation as before, in an unchanged environment; only Minecraft's
own Java (the official launcher's, or one downloaded from Mojang) is
passed on.
Also skip the Mojang download on x86-64 Linux without glibc's loader
(NixOS without nix-ld, musl distributions): Mojang's Java can't start
there, so say so instead of downloading 115 MB first. And keep each
runtime home once (dedup only dropped neighbours).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Serializer 17 (particles) and 16 hit a catch-all that returned Ok(None),
which stopped EntityData::read from walking the rest of the metadata list.
On armor stands that carry a level-particle effect slot, slots 2 and 3
(CustomName text and visible flag) were silently discarded, so serverside
holograms (Gamster's social-media panels and leaderboards) rendered as
nothing.
Implement both serializers as value-preserving decodes: a ParticleData
enum covering every 26.3 particle shape (state varints for ids 1/2/36/121/125,
colours, dust, dust_color_transition, effect/instant, scalar, shriek, trail,
item, vibration and geyser variants) backed by a static minecraft:particle_type
id table, with write_value arms for round-trips.
Co-Authored-By: cairn-code (GLM-5.3-Flash Free) <[email protected]>
to_stack kept only damage, dyed colour, map id and a head profile, so a server item's custom name, lore, enchantments and custom data never reached the inventory tooltip. Build the vanilla `components` compound from the decoded patch and run it through the same decoder saved item NBT uses, then show lore lines between the name and the details.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
On a Mac without a JDK, preparing failed with "Java 25 is needed ...
/usr/bin/javac -version printed no version: Unable to locate a Java
Runtime": /usr/bin/javac is macOS's placeholder. Mojang's Java runtimes
are whole JDKs, javac included (java-runtime-epsilon is 25.0.1 on every
platform), and the official launcher has already downloaded one for
anyone who played 26.3.
- horizon-prepare: java::find also tries Minecraft's runtimes
(<game dir>/runtime/<component>/<platform>/<component>, jre.bundle on
macOS) and the launcher's pick (Options::java_home), and names the
macOS placeholder for what it is.
- launcher: when no JDK is found, downloads the runtime the version JSON
names (javaVersion.component) from Mojang into its own game folder,
every file checked against its sha1, executables and links as the
official launcher lays them out. A version's own preparer binary or
Python script gets it through JAVA_HOME and the PATH.
Checked for real on Linux: the download is 147 files, its javac 25.0.1
compiles and runs a class with --release 25.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AxbYUJt4ShJFCdyLNCxF21
A marker armor stand has an empty box, so vanilla's shouldRenderAtSqrDistance computes a zero range and the model never draws; Horizon substituted a 4-block range, which turned the stacked marker stands a lobby uses for holograms into a visible tower. Follow vanilla and let the zero edge cull the model. Server NPCs and mobs also carry CustomName, CustomNameVisible and the shared invisible flag in entity data; Horizon only read those for armor stands. Apply them to every mob, keep the styled component for the name tag, and hide the body when the flag is set.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
A server lobby draws its signs as walls of framed maps and puts player heads on its NPCs. Horizon dropped every map_item_data packet, so framed maps stayed blank, and the item component decoder rejected attack_range (30) and profile (72), which made the whole equipment and container packets that carried them undecodable. Decode both components, carry a profile into the item data the head model reads, and apply server map patches to the map texture. The map pool now holds a wall of maps so the atlas does not re-upload every map every frame.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
Holograms are invisible armor stands carrying a CustomName. Armor stands parsed neither CustomName nor CustomNameVisible, and name tags came only from mobs, so a hologram drew nothing. Read both fields from NBT and entity data, keep them across a save, and put armor stands in the name tag pass at their bounding-box height even when their model is skipped.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
The tab list resolved skins only for listed players and never handed them to the level's player models, so every player and NPC in a server world kept the default Steve or Alex texture. Resolve every profile's textures each frame and give each player entity its skin, including entities that are never in the tab list. Player heads read the minecraft:profile component with the insecure lookup vanilla uses, so unsigned textures from offline servers draw too.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
winit's macOS set_cursor_position re-associates the mouse with the
pointer (CGAssociateMouseAndMouseCursorPosition(true)), so warping to
the window centre right after the Locked grab released it again and the
pointer moved freely in first person. Warp before grabbing on macOS;
Wayland still warps after, since it moves a pointer only while locked.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0188LkUPntcZdfMF3wAqR2tK
TerrainRegion::new refused a region whose chunks bounding box passed
2^20 columns, where it took any set of distinct chunks before: the
bulk_section parity fixture puts a chunk at x = 4194304 beside the
origin and failed with InvalidChunks. Keep the grid for the usual small
box and fall back to a map past it; both refuse a position twice. The
lookup now subtracts in i64, so no coordinate can wrap into the grid.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 Mth.floor(double) is (int) Math.floor(v), so a value below
i32::MIN floors to i32::MIN. The truncate-and-step version wrapped to
i32::MAX there (block_collision_scan parity failed at the chunk of
x = -2147483648.8: Rust 134217727, vanilla -134217728). Step down with
saturating_sub; the fast path is unchanged, and the test now checks
the ends of the i32 range, infinities and f64::MIN/MAX against
f64::floor as i32.
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]>
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15): fmt, clippy, workspace + launcher tests, release build and every affected parity harness pass. main merged into the branch with the same resolution as the tested tree; verify passes on that head.
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15): fmt, clippy, workspace + launcher tests, release build and every affected parity harness pass. main merged into the branch with the same resolution as the tested tree; verify passes on that head.
ExtraCover::new checked only x0/x1 against the i32 range, so two areas
near i32::MAX or i32::MIN in z built a grid whose row arithmetic
(cz + dz) overflows: a panic in debug builds, wrong cells in release.
Check z the same way, and test both ends.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
The daily promotion moves to 20:00 UTC (21:00 in Paris in winter time):
PROMOTE_AT and promote.yml's cron (20:07, retries until 21:37), with the docs
that give the time. A launcher left open on Nightly looks for new releases
every 30 minutes; Stable keeps 4 hours. The launcher README's Settings
screenshot shows the Updates choice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher's "Beta builds" switch becomes an Updates choice of two channels,
stored as "update_channel": "stable" | "nightly" (an old "beta_builds": true
reads as nightly). The CLI (update-channel) and MCP (update_channel) show and
set it, and list each release's channel as stable or nightly.
Nightly ships constantly: the release train waits 5 minutes for a merge wave to
settle (20 at most) instead of 15 (an hour). Stable moves once a day:
promote.yml runs at 19:00 UTC (retries until 20:37) and promotes the newest beta
that had been out 6 hours by 19:00, so late runs and retries pick the same one.
The stalled-promotion issue waits 30 hours. Release notes, docs, the website's
install and FAQ pages say Stable and Nightly.
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]>
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108#1114#1131#1157#1158#1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108#1114#1131#1157#1158#1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108#1114#1131#1157#1158#1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108#1114#1131#1157#1158#1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108#1114#1131#1157#1158#1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108#1114#1131#1157#1158#1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
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]>
Pack-wide numbers after the modpacks-12 PRs (#1175, #1176, #1177, #1191):
structures that start, processors carried and dropped per mod, content,
load time and the play-through, and the gaps ranked by how much a player
notices them.
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]>
A content pack's first start rewrote 19 per-state tables one after another
and reopened each zip mod's archive for every file it copied (its whole
index each time). Every start then decoded the atlases' mod textures and
parsed the mods' block models one file at a time on the main thread: with
ATM10's 42 data mods that was about 70 % of the sampled startup.
- mod_content: the tables are rewritten on up to three threads, largest
first; a zip mod's archive is opened once for all its files
(horizon-mods' Content::reader).
- hud::atlas_sprites and ParsedModels::load list their files first, then
decode and parse them on every core (par::par_map, results in the input
order). The sprites come out in the order the one-by-one loop gave, and
the content root is byte-identical.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The last two workstation menus guests couldn't use, both now on the
shared menu engine, which single player runs too:
- Lectern: LecternMenu on the engine (Kind::Lectern): the page buttons
(1, 2, 100+) and Take Book (3, mayBuild) as ContainerButtonClick, the
page as data slot 0 clamped to the book, the book in its one slot
(removeItem, setItem a no-op, max 1). The host opens minecraft:lectern
for a guest, applies the menu to the block entity (powered pulse, page
turn level event 1043, has_book), resends changes and closes it on
stillValid. Guests drive it from the book screen.
- Cartography table: CartographyTableMenu on the engine (Kind::Cartography,
setupResultSlot, onTake, onCraftedBy's new map ids through a Maps env),
replacing the old station code; guests get the same through
guest_menu_op.
- LAN item stacks carry map_post_processing and writable/written book
contents (they were blank for guests); map_post_processing is no longer
saved (ItemStack.CODEC never saves it).
- A guest who enchants gets the enchant_item stat.
- Click parity: VanillaMenuOracle and menu_fixture gain both menus.
- lan_e2e: new lectern and cartography checks; enchanting checks the
stat; server_count/server_reply ignore the guest's own operator
broadcast echo on --server-thread; the grindstone check counts levels
as well as points.
Fixes#868
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Linux an unprivileged thread can't raise itself, so #1179's render
thread hint does nothing there. The dedicated server already lowered its
helper threads instead (world generation, lighting, connection writers:
10 nice levels, or the idle class). The client now does the same, so the
world-generation and meshing workers (two pools with single player's
server thread) no longer hold the render thread back while terrain loads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Trees of neighbouring chunks overlap on chunk corners, and the one
decorated first wins. With several worker threads vanilla decorates in a
different order each run: seed -2080727421395798566 spawned at Y 64, 66
or 72 over 10 runs. The harness retried vanilla up to 3 times and passed
if any run matched, which still failed when none of them gave 72.
With -Dmax.bg.threads=1 (Util.maxAllowedExecutorThreads) vanilla
decorates in its scheduler's order, the one Horizon follows: that seed
gave 72 in 10 of 10 runs, as Horizon does. The other seeds that varied
are fixed too (10 of 10 each: -8171837717659076940 at 0 67 0,
-5296387317982839690 at -16 73 16, -211473008665507967 at 0 66 112).
The retries go, and vanilla's one spawn is compared exactly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three races made the two sides differ with nothing wrong in Horizon:
- Steps ended on a fixed 1.5 s sleep. On a slow run a chat or its
broadcast (vanilla queues the chat, then the broadcast as a later
task) landed in the next step. Each step now also ends on a barrier:
every connected client sends `random value 1..2` twice, and waits for
the answer, which only the sender gets and which is left out of the
transcript.
- The client closed every socket at once, and the server logs
"<name> lost connection" in whatever order its network threads see
the closed connections. The players still connected now leave one at
a time: each waits until it has been told the previous one left
(PlayerList.remove broadcasts that after logging). The harness waits
for Carol's line before stopping the server.
- A player the server kicks (Bob, Mallory) is logged as
"lost connection: <reason>", or as "Disconnected" when vanilla's
network thread closes the connection before disconnect(reason) runs.
The reason sent is still compared in the transcript, so the log line
no longer compares it for kicked players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Natural spawning was a quarter of a 40-player server thread. Every spawn
attempt cloned the position's whole spawn list (NaturalSpawner.mobsAt) to
check the picked entry is still in it, and every group cloned it again to
pick from; mobs_at now returns the biome's or the structure's list as it is
stored (the fortress list lives in a static). The nearest player's distance,
asked on every attempt, is now the minimum of the distances instead of a
search that kept the player too. Same lists, same draws, same distances.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player on the server thread publishes inside the integrated
server, so self.lan alone missed it; and the fps block runs only when a
frame is drawn.
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]>
floor_i32 is now Java's Mth.floor (truncate, step down for negative
fractions): f64::floor is a libm call on baseline x86-64, made for every
Perlin sample. Perlin's gradient table is stored as floats (every
component is -1, 0 or 1, exact either way), so the corner dot products
skip three conversions each. About 10% fewer instructions generating
terrain; values unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
The vanilla protocol client moved on after a fixed time per step. On a
busy machine, either side's server could be slower than that: the
summoned villager was not seen yet when the trade step ran, the furnace
had not smelted the ingot when it was taken, a snowball was not thrown
yet, or the pearl had not landed. Each mode failed now and then on one
side only.
Those steps now keep the same minimum time and then also wait, for up to
30 s, until it happened: the villager is tracked, the furnace's result
slot has the ingot (plus 1 s), the snowball entity was added (plus 1 s),
the pearl teleport and the health update arrived. The respawn line also
leaves out block updates: a lava fall keeps flowing while the game runs,
and how many updates arrive before the respawn depends on timing.
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]>
The client asks for its render thread what it already asks for the
server thread (vanilla's MinecraftServer.spin): user-interactive QoS on
macOS, above-normal priority on Windows, on machines with more than four
processors. While terrain loads at 32 chunks, the worker pools (two with
single player's server thread), the chunk encoders and other programs
outnumber the cores. A frame that gave its core up in queue.submit then
waited 10 to 200 ms to get one back: the long frames of #678.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_lan described every tracked entity afresh each tick: a new String for
its type, new vectors for its entity data, equipment and attributes, all
freed again at the end of the tick. The states now live in the LAN host
between ticks and each tick writes over the last tick's in place
(lan::entity::States), so those allocations are kept. The states, and so
every packet built from them, are the same as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game sends an anonymous ping once a minute while it runs (a random
session ID kept only in memory, the version, the platform, whether a LAN
or friends world is open). The telemetry server counts sessions seen in
the last 2 minutes, in memory only, with its own per-address rate limit
and a session cap, and serves rounded counts at GET /v1/live. The
launcher shows "N playing now" on the Play page when the count is
reachable, and tells players what the ping holds on the first-run
question, in Settings, and once on the Play page for players who
answered before. docs/telemetry.md and the privacy page say the same.
Like the rest of telemetry, nothing is sent until HORIZON_TELEMETRY_URL
is set or DEFAULT_ENDPOINT gets a server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three races made the comparison of the servers' lines about the player
fail at random:
- a login's lost connection is logged at the server's next tick, and the
next login could start before it: each login now ends once the server
logged its lost connection;
- vanilla's authenticator disconnects a player, then logs why, while the
server thread may already log the lost connection: each login's lines
are compared thread by thread;
- a player the server kicks closes the connection once it reads why; when
that comes first vanilla logs the lost connection as "Disconnected"
(seen once in 12 desktop runs), so a login's kick reason stands for both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ThrownEnderpearl.onHit teleports the owner to oldPosition() (where the
pearl started its last tick) with ServerPlayer.teleport, which sends the
position packet whatever the distance. A guest's turn sent a teleport only
when the player moved more than 0.01 blocks, and the pearl took the hit
point: a pearl thrown at one's feet often lands within that, and the
dedicated_server harness saw "teleported 0 time(s)" where vanilla's server
sends 1 (about one run in four on the desktop).
The pearl now moves the player to its previous position and marks the
teleport (PlayerMotion.teleported), which the end of the guest's turn
sends even when the player stays put.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client of a server (single player's server thread, LAN, multiplayer)
handled every received packet in the frame it arrived. Crossing a chunk
border at 32 chunks brings 25 to 60 columns at once, 0.1 ms each to
decode on an M4 Pro: a 3 to 8 ms frame each time. Once a frame has spent
1 ms on chunk packets, the events from the next chunk packet on wait for
the next frame, in order; events that have waited a tick are all handled
at once, so a slow machine's backlog never holds packets longer than
that. The status's pending_chunks counts the waiting events.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Formations' PiecedStructure starts like its SimpleStructure, then lays out
the element's jigsaw children with vanilla's jigsaw. The game's jigsaw
assembler now exposes its child layout (add_children) for a start piece
placed by another structure type; horizon:formations reads horizon:pieced
(max_depth, max_distance_from_center), which carry writes for
formations:pieced.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ctov:modular_compat runs another processor list only when the mod it names
is loaded: carry writes that list in when the mod is ported along and takes
the hook out otherwise (142 copies in ATM10, all for Nature's Spirit).
Repurposed Structures' mineshaft_sky_view_processor (71 lists) becomes the
game's horizon:mineshaft_sky_view: nothing at or above the world's
OCEAN_FLOOR_WG height, except plain rails over a block that can occlude,
waterlogged where the world holds water. Placement contexts gain
can_occlude.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's use of a crafter now opens CrafterMenu (minecraft:crafter_3x3)
over the block entity. The host runs its clicks on the menu engine's
verified Kind::CrafterBlock (a disabled slot refuses items, filling one
enables it), sends the disabled slots and triggered as data slots
(CrafterBlockEntity.containerData), and handles
ServerboundContainerSlotStateChangedPacket (new in horizon-protocol) as
CrafterBlockEntity.setSlotState. The guest shows the server's crafter,
predicts its clicks and sends a slot toggle before the click as
CrafterScreen.slotClicked does.
Part of #868.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BeaconMenu now runs on horizon_client::menu (Kind::Beacon): the payment
slot (beacon_payment_items, one item), quickMoveStack and updateEffects
with BeaconBlockEntity.validateEffects, verified against the vanilla class
by test_menu_click_parity with a new SET_BEACON op (valid and invalid
powers for each pyramid level).
ServerboundSetBeaconPacket is now in the protocol. The LAN host opens
minecraft:beacon for guests with the block entity's levels and powers as
data slots (kept current while the menu is open), applies a guest's
powers to the block entity (power_select while the beam is up) and drops
the payment when the menu closes, as BeaconMenu.removed does. Guests show
the server's levels and powers and their Done button sends the packet.
Single player's beacon clicks go through the engine too.
Part of #868.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnchantmentMenu now runs on horizon_client::menu: the item slot (one
item) and the lapis slot, its quickMoveStack, slotsChanged clearing the
offers without an enchantable item, and clickMenuButton's checks against
the costs, lapis and levels, verified against the vanilla class by
test_menu_click_parity (with its ten data slots and the offer buttons;
the oracle's player now has a profile for the invalid-button log line).
The offers and the enchanting itself, which need a level, stay with the
caller.
Single player routes the table's clicks, drags and double clicks through
the engine. The LAN host opens minecraft:enchantment for guests with
their own enchantment seed (saved with their player data), sends the
costs, seed and clues as data slots, and on ContainerButtonClick
enchants the item, takes the guest's level and lapis and gives a new
seed. Guests show the server's offers and send the button the client
menu accepts.
Part of #868.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AnvilMenu and SmithingMenu (ItemCombinerMenu) now run on
horizon_client::menu: their slot rules, createResult (the anvil's cost
data slot, repair count and rename, the smithing table's hasRecipeError),
mayPickup with the player's levels, the results' onTake and
quickMoveStack, verified against the vanilla classes by
test_menu_click_parity with repairs, merges, renames (a new RENAME op,
AnvilMenu.setItemName) and the player's levels. The harness found the
anvil treating only enchanted books as books (vanilla: anything with
stored enchantments), now fixed, and the grindstone's and anvil's result
slots now count for a double click's pick-all as in vanilla.
Single player routes both screens' clicks, drags and double clicks
through the engine; the anvil's wear and level events 1030/1029, and the
smithing table's 1044, play with vanilla's random pitch.
The LAN host opens minecraft:anvil and minecraft:smithing for guests,
handles ServerboundRenameItemPacket (new in horizon-protocol), charges the
guest's levels and wears the anvil; guests send the rename box as
AnvilScreen.onNameChanged does and show the server's cost. The control
socket's `type` now reaches the anvil's rename box.
Part of #868.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GrindstoneMenu now runs on horizon_client::menu (Kind::Grindstone): its
input filter, computeResult, the result's onTake (both inputs cleared,
the experience sum handed to the caller) and quickMoveStack, verified
against the vanilla class by test_menu_click_parity with enchanted,
damaged and cursed tools and books.
Single player routes the grindstone's clicks, drags and double clicks
through the engine and rolls the orbs and level event 1042 (now playing
block.grindstone.use) where the result is taken. The LAN host opens
minecraft:grindstone for guests on the same engine and awards the orbs at
the block; guests read enchantments, stored enchantments, repair cost and
custom names from the server's stacks, and the host now sends repair
cost.
Part of #868.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
StonecutterMenu joins horizon_client::menu (Kind::Stonecutter): the input's
recipe list (slotsChanged / setupRecipeList), selectedRecipeIndex as its data
slot, clickMenuButton (an invalid index is taken too), the result's onTake
(one input used up, awardUsedRecipes, the take sound) and quickMoveStack with
the leftover of a result move dropped. test_menu_click_parity compares it
with vanilla's StonecutterMenu, recipe buttons included.
Single player's stonecutter screen now clicks through the engine (drags and
double clicks included), and its recipe grid calls clickMenuButton as
StonecutterScreen does. A LAN guest's stonecutter opens minecraft:stonecutter
on the host's engine, as the loom does (#868).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's use of a loom opens LoomMenu (minecraft:loom) for the guest. The
host runs the guest's menu on the shared menu engine (horizon_client::menu,
Kind::Loom, as single player does): clicks, the pattern buttons
(ServerboundContainerButtonClickPacket), the content and data slot sync after
each, the take-result sound at the loom, stillValid (the block gone or out of
reach) and clearContainer when it closes or the guest leaves. The guest opens
the loom screen for the server's menu, predicts its clicks and buttons on the
engine and sends them, runs the menu's client-side slotsChanged on the
server's slots and reads the selected pattern from the data slot.
The framework (game/lan_stations.rs, game/remote_stations.rs) takes the other
workstations one by one (#868).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Carry maps mostructures:generic to vanilla's jigsaw with horizon:start_checks
(terrain_radius 2, terrain_range = heightRange) and mostructures:type to a
random spread with the config's default spacing and separation and its sets
to avoid as horizon:exclusion_zone. The game's exclusion zone gains
"their_zones": false, which tests the avoided sets by spread and frequency
alone, as ModStructurePlacement does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#1007's white overlay on Hyprland is #1075's washed-out swapchain: a
Wayland compositor with colour management makes Mesa list Rgba16Float
first, and the first non-sRGB format was taken. Reproduced on a real
GPU (Mesa 26.2 ANV, Intel HD 620) under headless weston 16: the old
choice washes the frame out, main's Bgra8Unorm doesn't. This test keeps
that list, as wgpu reports it, choosing an 8-bit format.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TerrainRegion indexes its chunks through a grid over their bounding box.
OreFeature asks the world whether any column under its blob reaches the
floor heightmap in one call, and tracks tested positions in a bitset.
BiomeTemperature no longer preallocates its map and queue. Same output.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
A version prepared while the sound copy failed (Mojang's republished 26.3
asset index, #1076, fixed in #1091), or one that reused such a version's
resources when installed, stays silent and showed as plain Prepared. The
Play page now says it is prepared without its sounds and makes Prepare
again the visible fix; versions --installed and the MCP list say so too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client asks for its render thread what it already asks for the
server thread (vanilla's MinecraftServer.spin): user-interactive QoS on
macOS, above-normal priority on Windows, on machines with more than four
processors. While terrain loads at 32 chunks, the worker pools (two with
single player's server thread), the chunk encoders and other programs
outnumber the cores. A frame that gave its core up in queue.submit then
waited 10 to 200 ms to get one back: the long frames of #678.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HORIZON_SPIKES spans keep the thread's CPU time beside their wall time,
and the slow-frame line gives the render thread's CPU time in the frame.
A span that mostly waited (for the GPU, a lock or a core) prints both
(`submit 30.2/0.1`), so a frame that waited reads apart from one that
worked. Found the cause of #678's long frames: the render thread losing
its core in queue.submit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Releasing a Riptide trident now starts LivingEntity.startAutoSpinAttack(20,
8.0): for 20 ticks the player takes the SPIN_ATTACK pose (0.6x0.6, eye
height 0.4), hits the first living entity its swept box touches for the
spin's damage and bounces back (velocity x -0.2), and stops on a horizontal
collision. The player model spins (setupRotations: rotX(-90 - xRot),
rotY(age * -75)), SpinAttackEffectLayer draws the riptide boxes, and the
first-person hand uses ItemInHandRenderer's spin transform. LAN guests see
the host's spin through living flag bit 4.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#1113 (moving pistons) and #1115 (break particles, which gave particle_sprite
a position for its biome tint) each passed alone; together main no longer
compiled. A moving block now takes its tint at its own block position, as
vanilla's moving-block renderer reads the level's tint there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify and CI parity pass (the earlier online_mode failure was a race and passed on rerun). Moving blocks are drawn and solid; entity pushing is in FEATURES.md LATER 4h.
🤖 Agent: merged. Verify passes. Proof: the B5 integration of these eight heads (parity --changed 102/102, lan_e2e 46–47/48 with only order-dependent flakes), and the conflict resolution in tools/lan_e2e.py checked def by def against each branch.
🤖 Agent: merged. Verify passes; local parity --changed origin/main passes (101 harnesses; #1159's one online_mode diff reran clean and passes on main). Screenshots and tick-by-tick results in the body.
🤖 Agent: merged. Verify passes; local parity --changed origin/main passes (101 harnesses; #1159's one online_mode diff reran clean and passes on main). Screenshots and tick-by-tick results in the body.
A new Horizon Extras option, Lighting > Dynamic lights (`dynamicLights`,
off by default): a torch, lantern or other light-emitting item held by a
player or mob, dropped as an item, or any entity on fire lights the blocks,
entities and hand around it, as LambDynamicLights and OptiFine do.
Client-only and at render time: the light engine, chunk meshes and saves
never see it. Each frame the game picks the 16 sources nearest the camera
(dynamic_light::select) and passes them in Globals; terrain.wgsl and
entity.wgsl raise each vertex's block light to `level - distance` when that
is brighter. Off, the count in globals.time.z is 0, nothing is gathered and
the shaders' loop runs zero times, so no pipeline is rebuilt either way.
An item glows like its block's default state (torch 14, lantern and
glowstone 15, lit redstone torch 7); a lava bucket 15; burning entities 15.
Fixes#814
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The guest came home into its Nether portal's frame, with an obsidian
ceiling three blocks up, and the host onto the End portal it had set at
spawn; both stayed there for the rest of the run. Under that ceiling a
double jump rises about 1.2 blocks at most, and whether the flight check
saw it depended on timing: `flight` failed in full --server-thread runs
(portals runs only there) and passed alone. The check now clears the
frame and the End portal in the overworld, and puts the guest back where
it started, also when it fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On single player's server thread the broken log's drop and its pickup
both come from the server, a few ticks after `break` answers: the
pickup could land before the scenario looked for the drop, which it
then never found (StopIteration). Step 3 now waits up to 20 ticks for
the logs in the inventory or the drop to walk to.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 2 clones a stack in the creative tab and closes the screen. The
clone lived only on the client: ItemPickerMenu's carried stack is the
inventoryMenu's, and AbstractContainerMenu.removed clears the carried
stack only for a ServerPlayer, so vanilla's client still holds it when
the survival inventory opens. With a stack in hand the middle button's
release sends CLONE (AbstractContainerScreen.mouseReleased); the server
finds the carried stack it was told about wrong and sends the empty one
(broadcastChanges). On single player's server thread that answer takes
a round trip, as in vanilla, so step 3 now waits up to 20 ticks for it.
The in-frame world puts the clone away when the menu closes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From b9f3821f (fix/server-thread-default): the host window draws what
it was sent on the server thread, and agent_control_e2e waits two ticks
where the server answers a click.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a client of a server (single player's server thread, a LAN guest) the
placed block comes with the server's answer: `place` waits up to a second
for it before reporting no change. agent_control_e2e tries the next free
spot when the tree's trunk above the broken log hides a spot's faces.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 9821bb7ef5)
- The command harness (HORIZON_COMMAND_SCRIPT) drives the world itself,
without the event loop, so it does not start the server thread.
- `status` lists the minecarts a client of a server was sent.
- save_roundtrip: single player's server thread finishes the chunks its
player tracks, as vanilla's integrated server does, including some that
vanilla's server (view distance 4) left before their terrain or light.
Those had no blocks, light or heightmaps to compare yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2818c89f24)
Single player's world now runs on the integrated server thread unless
asked otherwise, as vanilla's does, so a world tick no longer holds up
a frame. `--no-server-thread` or `HORIZON_SERVER_THREAD=0` keeps the
world in the frame as an escape hatch; `--server-thread` stays.
lan_e2e.py sets the host's mode explicitly either way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 4ff23b686d)
🤖 Agent: merged. Verify passes; CI parity's only failure is the known dedicated_server pearl flake. Measured panning against OpenAL Soft's stereo output in the body.
🤖 Agent: merged. Verify passes. CI parity's only failure is server_player_targets, a server harness this client rendering change can't touch; it has flaked before (its log said '109 lines identical' on #1118). Screenshots in the body.
🤖 Agent: merged. Verify and CI parity pass. Mod browser part 1/3: Modrinth source and checksum verification. The CurseForge source stays inactive until a key is configured.
A player who joined already in creative could not fly: the remote
client kept only instabuild and the flying speed of
ClientboundPlayerAbilitiesPacket, and the motion step clears flying for
a player that may not fly. The packet also comes before the client has
a player (it is made at the first teleport). Single player creative on
its server thread could not fly at all; a LAN guest only after a game
mode change.
- The remote client keeps the last abilities and applies flying, may
fly, invulnerable and the flying speed to the player, also when the
player is made (ClientPacketListener.handlePlayerAbilities).
- When its flight turns on or off (double jump, spectator, landing) it
sends ServerboundPlayerAbilitiesPacket (LocalPlayer.onUpdateAbilities).
- The hub keeps it as the guest's flying when the guest may fly
(handlePlayerAbilities), so the save has it, and a game mode change
sets it as GameType.updatePlayerAbilities does.
- lan_e2e `flight`: the guest, and on the server thread the host's
window, fly in creative, stay up, and stop in survival.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PerlinNoise::add_volume worked out each Y coordinate's cell, offset and
fade once per X/Z column. Every column walks the same Y coordinates, so
they are now computed once per volume. The arithmetic is unchanged, so
the output is bit for bit the same.
ClimateIndex kept its tree as nodes with a Vec of child ids each. The
built tree is now renumbered breadth first into flat arrays, so a
branch's children are one contiguous run. The search visits the same
nodes in the same order, so ties resolve exactly as before.
terrain_bench (seed 12345, 8x8 chunks, 1 thread, medians of 3 runs):
terrain 14.5 -> 12.4 ms/chunk, biomes 1.20 -> 1.06 ms/chunk.
Checked against vanilla 26.3 in-process: 2304 chunks on seeds 12345, -1
and 987654321987 identical, and the biome-source and biome-generation
parity suites (2.1M lines) match.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
Vanilla plays a block use's sound with `level.playSound(player, ...)`: the
player who clicked hears it from their own client's use, and the server
sends it to everyone else. Horizon's guest waited for the server's sound,
one round trip after its predicted door had already opened.
The guest now plays the sounds of the use it predicts (doors, trapdoors,
fence gates, levers, buttons), and the server sends the sounds of such a
use to the other players only. The control socket's `status` counts the
positional sounds each game played (`heard`).
lan_e2e `predict`: with the host frozen, the guest already hears its door;
once the server answers, the guest and the host have each heard it once.
Fixes#869
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client of a server sent the bow's use but never started using the item
itself, so letting go of the button sent no RELEASE_USE_ITEM: the server
kept the guest drawing and never shot. BowItem.use runs on the client too
(startUsingItem, with an arrow or in creative), and Minecraft.handleKeybinds
releases the item in use once the button is up.
The guest now draws on its own side as well (held until the button lets go
or another slot is picked), so the server shoots when it lets go.
lan_e2e: `arrows` passes both ways now (the guest's arrow hurts the host);
the `pvp` and `arrows` checks allow the victim natural regeneration while
pvp is off (it healed 19.3 -> 20 and failed the exact check).
Fixes#484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's MultiPlayerGameMode.useItemOn runs the use on the client's own
level inside startPrediction, records each block it changes under the
packet's sequence in BlockStatePredictionHandler, and keeps them until
ClientboundBlockChangedAckPacket settles them (a use the server refused
goes back). A Horizon guest only sent the use: its door opened one round
trip later.
The guest already ran the use on a dry copy of its level to know whether
the click ends there; the blocks that use changes are now kept on its
level as predictions under the use's sequence, as the break prediction
does, when they are all hand-operated blocks: a wooden or copper door,
trapdoor or fence gate, a lever or a button (an iron door has no hand use
and changes nothing). The sound still comes from the server.
lan_e2e `predict`: with the host process frozen, the oak door the guest
clicks is already open on the guest's side (both halves), stays open on
both sides once the server answers, and an iron door stays shut.
Fixes#869
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A remote player's projectiles never hit other players: a player's shots
only looked for mobs and the host, a guest's flew as if the host had shot
it (so it could not hit the host until it left the host's box), and a hit
on a guest from another guest ran as the shooter, who has no hub to reach
the victim. No bow PvP on a server or between LAN players.
A player's arrows, snowballs and eggs now meet every player, a guest's
flying from that guest (Projectile.getOwner). A hit on another player runs
as the victim's hurt, refused when the pvp game rule is off or the teams
are allies without friendly fire (ServerPlayer.canHarmPlayer), which turns
the arrow back as a raised shield does (AbstractArrow.onHitEntity).
Pearls, potions and wind charges fly past players as before.
lan_e2e `arrows`: the guest shoots the host and the host the guest with a
bow, both hurt; with pvp off, no harm.
Part of #484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.die credits the kill to lastHurtByPlayer: the killed
statistic, the mob_kills custom statistic, the kill score criteria and the
player_killed_entity trigger belong to that player. Horizon kept only
whether a player had hurt the mob, and the death ran as the host, so a LAN
or server guest's kills counted in the host's statistics and advancements.
The mobs a player hurt now remember which player (the acting guest's
connection, or the host), and the killer's side of the death runs as that
guest.
lan_e2e `kills`: a pig the guest kills shows in the guest's
minecraft.killed:minecraft.pig score, not the host's.
Part of #484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub's players are server players too, and in vanilla the armour they
wear counts against every hit (LivingEntity.collectEquipmentChanges puts
each piece's attribute modifiers on the player, getDamageAfterArmorAbsorb
reads them). Horizon only put them on the host: a guest who put armour on
in its inventory menu (the hub moves it), joined wearing it, respawned with
it kept or came through a portal (a new body each time) was hurt as if it
wore nothing. Iron armour, 15 points, should take a 10-damage blow down to
6; a guest took the full 10.
Each guest now keeps its own list of applied equipment modifiers, swapped
in with the rest of its state for its turn, and its armour and main hand
are put on its attributes whenever they differ from what they were put on
for (a new body starts with none).
lan_e2e `worn`: the guest shift-clicks iron armour on in its inventory and
takes 10 mob-attack damage: 6 with it, 10 without (failed before: 10 and 10).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit e0549f88ce)
ServerExplosion.hurtEntities takes every entity in range, players
included, but only the host player was hurt and pushed: LAN guests stood
in a blast untouched. The player's part is now its own function, run for
the host and then for each guest in the level, swapped in as the player.
It also follows Player.hurtServer more closely for everyone: the damage
goes through a raised shield (an explosion is blocked like any attack
from where the blast is, the shield takes wear), the push is reduced by
explosion knockback resistance, and a spectator or a creative player
flying is not pushed.
tools/lan_e2e.py gains a `blast` check: primed TNT five blocks from a
survival guest hurts it (20 -> 4.6) and pushes it away; facing it behind
a raised shield, the guest takes nothing. Before this change the guest
took no damage and no push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0782a598fe)
PiglinAi.throwItemsTowardPlayer aims at NEAREST_VISIBLE_PLAYER: the
nearest living, non-spectator player within 16 blocks. Only the host was
considered, so a LAN guest who bartered saw the loot fly to the host.
lan_e2e's new barter check: the guest, in gold boots, drops a gold ingot
to a piglin in a glass corridor with the host on the far side; the
piglin stays calm and the barter reaches the guest. It fails on the old
build (the loot lands on the host's side).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 896f12f732)
Player.attack on a #minecraft:redirectable_projectile (a ghast's fireball,
a wind charge) deflects it with ProjectileDeflection.AIM_DEFLECT: its
movement becomes the attacker's unit view vector and the attacker its
owner, with the no-damage sound. The crosshair finds these projectiles a
block round their box (AbstractHurtingProjectile.getPickRadius), a remote
client sends the attack for them, and the host turns one a guest strikes
along that guest's view. A fireball a player turned back kills the ghast
it hits (Ghast.isReflectedFireball: no fire immunity, 1000 damage).
lan_e2e.py --check deflect: the host strikes a caged ghast's fireball and
the ghast dies; before, the fireball hit the host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 6ba02f5205)
SmallFireball.onHitBlock places fire with Level.setBlockAndUpdate and
BaseFireBlock.getState (soul fire on soul soil and soul sand), and a mob's
fireball only when mobGriefing is on. Horizon wrote a plain fire state
straight into the chunk: no block updates and no scheduled fire tick, so
the fire never aged and never went out, even on stone or nether bricks
where vanilla's goes out within about half a minute (FireBlock.tick). Every
blaze fireball that missed left a fire that stayed for good: fortress
floors round a spawner filled with fire that kept everyone near it burning.
The fire is now the level's fire state for the spot, set with updates
(the fire's tick is scheduled on place), and only with mobGriefing when a
mob shot it.
lan_e2e `fireball`: a dispenser shoots a fire charge onto stone; both
players see the fire and it goes out by itself (failed before: still burning
after 1200 ticks; now out after about 480).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 5b7235c24b)
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
On a server every player is an extra area, and streaming asks of every
loaded and wanted column, several times a tick (unloading, dropping
protos, finishing, decoration, the generation scan), whether one of
those squares reaches it: a scan of every player's square per question.
The squares are now one grid of distances, made again when a player
changes chunk, and in_extra reads it. Differential test against the scan
over 60 random layouts, every margin.
At 50 players, 6081 columns, 3 questions each: 0.45 ms -> 0.04 ms, plus
0.02 ms to make the grid.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
HopperBlockEntity.suckInItems scanned every item entity for each hopper
that ticked without a container above. During the block-entity pass the
game now keeps the set of block columns item boxes reach; a hopper whose
column is not in it has nothing to suck, and the scan (same order, same
item taken) runs only for the rest. Items do not move within the pass:
removals leave a superset, and additions (pushed at the end with a new
id) or removing the last item rebuild the set. It is dropped at the end
of the pass.
500 hoppers, 200 items: 1.36 -> 0.05 ms per tick
2000 hoppers, 500 items: 13.52 -> 0.26 ms per tick
2000 hoppers, 2000 items: 60.6 -> 3.3 ms per tick
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
The Shader Pack setting cycled through what shaderpacks/ held, so with the
folder empty (or the packs in the launcher's version folder, as brabus_ had
them) it silently stayed Off. The row now opens a Shader Packs page: a
sidebar section in the Horizon menus, Video Settings > Shader Packs... in
the Classic ones. It shows the game home's shaderpacks/ path and an Open
Folder button (made if missing), says when there is no pack, and lists one
switch per pack. A pack that fails to load says why on its row. The list is
read again every second while the page shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GL fills the channels a fragment shader leaves out; wgpu refuses a pipeline
whose output has fewer channels than its target. Steadfast's final pass
writes `out vec3` and failed to build in the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preprocessor (GLSL and shaders.properties), as JCPP does:
- a #define replaces an earlier macro (Bloop, Steadfast: DefineRedefined);
- in #if, a float counts as its integer part (Steadfast's
shaders.properties float token), and a macro always defined empty counts
as 0 (Iris's MC_GL_VENDOR_OTHER, Bloop's TAA_USE_YCOCG);
- an #if may run to the end of the input (Bloop's taa.fsh);
- #error "..." lexes in skipped blocks (Photon);
- shaders.properties keeps its empty lines and hides backslashes from the
preprocessor (IRIS_PASSTHROUGHBACKSLASH), so a continued value no longer
swallows the next directive or line (Photon, Bliss, Steadfast's custom
uniforms; Complementary's isCold uniform is now complete).
Legaliser: % through overloads (naga won't take ivec2 % int: Noble),
sqrt of a negative constant left to the GPU (Bloop's NaN), constants naga
can't fold (?:, mix: Bliss) become globals set in main, and 16/8-bit types
(u16vec2 & co., Noble on NVIDIA) become their 32-bit ones.
horizon-client: real_packs_load runs the game's own load over the packs in
HORIZON_TEST_SHADERPACKS (the compatibility table's source).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Mods page between Versions and Modpacks: resource packs, shader packs and
modpacks as tabs, a search field, Show everything, and Modrinth / CurseForge
once CurseForge has a key. Results are index rows with summary, author,
downloads, source, licence and how well each runs (with the reason), Page and
Install (Import for a modpack, which then shows on the Modpacks page).
Installed packs list their version, source, file and the checksum they were
checked against, with Update and Remove. Searches, installs and update checks
run on threads; stale search answers are dropped.
Fixes#577
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102 (the explosion-dependent 22 rerun after rebasing onto #1101), and lan_e2e passes except checks that fail only after earlier checks and pass alone (in FEATURES.md LATER 4l).
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102 (the explosion-dependent 22 rerun after rebasing onto #1101), and lan_e2e passes except checks that fail only after earlier checks and pass alone (in FEATURES.md LATER 4l).
browser::installed installs a pack and its required dependencies into
resourcepacks/ or shaderpacks/, every file checked before any download
(a dependency that is a Java mod stops it) and checksum-verified on the
way in; mod-browser.json records source, version, file and checksum.
Update swaps the file, remove deletes only what the browser installed (a
player's own file of the same name is never overwritten). A modpack is
downloaded, checked, and imported through the Modpacks page's pipeline.
The CLI gains `mods list|search|install|update|remove`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher's core gains `browser`: search Modrinth for what Horizon loads
(resource packs, Iris/OptiFine shader packs, modpacks; never Java mod jars),
say how well each result runs, pick the newest version that runs, and
download a file only from Modrinth's or CurseForge's file servers, with a
plain file name, kept only when its size and SHA-512 (SHA-1 on CurseForge)
match. CurseForge is a pluggable source that stays off until an API key
exists (HORIZON_CURSEFORGE_API_KEY at run time or build time; the launcher
workflow passes the repository secret). docs/mod-browser.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three differences from vanilla's Axolotl kept the brain from matching:
- The constructor's setPathfindingMalus(WATER, 0) was missing, so every
swim target in water failed DefaultRandomPos's malus check and idle
axolotls never picked a walk target.
- Axolotl.getWalkTargetValue returns 0 everywhere; Horizon used Animal's
light-based value, which picked the brightest candidate instead of the
first.
- Axolotl.playAttackSound (entity.axolotl.attack at pitch 1) never played.
The mob AI harness gets #64's two scenarios, in a pool sunk into the ground
so no water flows out of it: hunting a cod, and playing dead after repeated
hits. Both now match vanilla on every tick (393/393 mob-ticks, was 26).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
sculk_sync_index collected every loaded chunk key into a set and diffed
it against the known chunks every ten ticks, thousands of keys on a busy
server. It now remembers the chunk map's membership revision and skips
the diff while it is unchanged; a replaced chunk keeps its key and was
not rescanned before either.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
tick_allays scanned every block entity of the level for each allay,
every tick, to find a playing jukebox within 10 blocks. The playing
jukeboxes are now collected once, at the first allay (no allay starts
or stops one).
piglin_pickups looked up the piglin_loved tag and hashed every item
entity's id once per piglin. Each item's flag is now computed once and
kept until an item is added (always pushed at the end under a new id)
or removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
GameLevel's isUnobstructed, noCollision (living shulkers) and the
spawners' entity count each scanned every mob, computing each 1.5 KB
mob's box. A spawn pass holds the game immutably, so after eight
queries it builds a grid of the mobs' boxes once and reads candidates
from it; the first queries still scan, as the build costs a few scans.
100 box tests, 2000 mobs: 3.48 -> 0.31 ms (grid build included)
Also gives the mob_ai_probe example the new others_index field.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
pois_in_range (AcquirePoi and the job, bed and meeting point searches)
asked, for every candidate POI in range, how many tickets it had, and
each answer scanned every other mob for villagers holding it: POIs times
mobs per search. The others' tickets are now counted into a map at the
first candidate and read from it; this villager's own memories are
still checked per POI, as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
FollowParentGoal (every baby, every tick), the pufferfish's puff check,
the enderman's block placing and the iron golem's DefendVillage scanned
every mob. They now read candidates from the position grid around the
area they test (one block wider where they test positions, as a box
whose bottom face holds the position meets it), in snapshot order, and
test exactly as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
On a server every ticked mob is bound to its nearest player, and each tick
the bindings of mobs that are gone were dropped by scanning every mob for
each binding. Mob is 1.5 KB, so the scan misses the cache on every mob:
with 1000 loaded mobs that was 0.44 ms a tick, with 2000 1.86 ms; a set of
the live ids makes it 0.02 and 0.04 ms (stand-in structs of Mob's size).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
tick_creature_entities called mob_world_base for every mob. That rebuilt
the player view (its allies set scans every mob UUID) and, with any
scoreboard team, an owned copy of every mob's team, each a clone of the
team's player set: cubic in the mobs on a team. The base is now built
once and cloned per mob, with the teams borrowed behind an Arc.
100 mobs, half on a team: 37.7 ms -> 0.06 ms per tick
500 mobs, half on a team: 4297 ms -> 0.31 ms per tick
MobWorld::seen looked mobs up by scanning the snapshot; the mob ticks
now pass an id index (first slot wins, as the scan did): 2000 lookups
over 2000 mobs 1.09 -> 0.01 ms. The nearest-living-entity sensors, the
look-at goal, the ravager's roar and MobWorld::nearby read candidates
from the tick's position grid instead of every mob, in the same order:
2000 mobs 0.39 -> 0.11 ms per tick.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
🤖 Agent: merged. Verify passes. CI parity's failures are only the known flakes: world_spawn (vanilla gives several spawn heights for one seed across runs) and dedicated_server (play-pearl). This client rendering change can't move either. B2's integration passed every rendering harness, and before/after screenshots are in the body.
🤖 Agent: merged. Verify passes. CI parity's failures are only the known flakes: world_spawn (vanilla gives several spawn heights for one seed across runs) and dedicated_server (play-pearl). This client rendering change can't move either. B2's integration passed every rendering harness, and before/after screenshots are in the body.
On a server or a world open to LAN, a player saw only their own crack.
The server sent each new stage to the others
(ClientboundBlockDestruction), but a client of a server ignored the
packet, and the host of a LAN world neither drew its guests' cracks nor
sent its own to them.
Vanilla keeps the cracks per breaker (ClientLevel.destroyBlockProgress:
a progress outside 0..9 removes it, 400 ticks without news drops it) and
draws, within 32 blocks, the furthest one on each block
(LevelExtractor.extractBlockDestroyAnimation). Horizon now does the
same: a client of a server takes the packet, a LAN host records its
guests' stages and sends its own (each new stage, then -1 when it
stops), and every crack is drawn with its own stage. A guest's crack
also goes when the block it digs is gone before its STOP_DESTROY_BLOCK
(its own tick broke it), as ServerPlayerGameMode.tick clears it on air:
the others kept its last stage for 400 ticks. Each crack vertex carries
its stage sprite's atlas origin in its light bytes (the crumbling pass
is unlit); the shader keeps the shared sprite size.
lan_e2e `cracks`: each side watches the block the other mines and sees
its crack grow; no crack is left once the blocks break. The screenshots
from both sides show the cracks.
Fixes#1096
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pushes between mobs wait in a list until their target ticks; each mob's
tick scanned the whole list, and take_mob_pushes removed each push from
the events in place, shifting the rest. In a pen where every mob pushes
its neighbours both grow with the square of the crowd per mob.
The pending pushes stay a list while short (64) and become one list per
mob past that, each in push order, so every velocity adds up the same
values in the same order. take_mob_pushes splits the events in one pass,
keeping the order of the others. A test checks velocities bit for bit
and the remaining events against the old code.
30 mobs pushing each other: 0.11 -> 0.04 ms per tick
100 mobs pushing each other: 1.73 -> 0.37 ms
200 mobs pushing each other: 9.07 -> 2.61 ms
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
Every lit furnace, smoker and blast furnace called Recipes::cook each
tick, which scanned all cooking recipes (string compares and hash
lookups) and the furnace then cloned the recipe found, its ingredient's
item set included. Recipes now keeps, built at the first lookup, the
first recipe of each kind for each item, which is what the scan
returned; the furnace borrows it.
1000 lookups over 320 recipes: 2.07 -> 0.08 ms
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
Two costs that grow with players on the server thread:
- Hub::update_view rebuilt a player's chunk queue on every chunk crossing
by scanning the queue (a Vec) once for each chunk of the view. It now
checks a set. Same queue, same order (differential test against the old
scan). 111 -> 16 us per crossing at view 10, 6.5 ms -> 0.13 ms at 32.
- ChunkTickets rebuilt a hash map of every ticketed chunk whenever any
player changed chunk, and every block entity, mob and ticking chunk
hashed into it. Levels are now a grid over the tickets' bounding box,
with the map kept for tickets spread too far apart. At 50 players:
build 0.23 -> 0.08 ms, lookups about 2x faster (differential test over
200 random ticket layouts).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
PotentSulfurBlock.animateTick sends two sulfur_bubbles up through a water
source above a wet potent sulfur block, and one time in ten plays
block.potent_sulfur.noxious_gas. SulfurBubbleParticle rises at -0.04
gravity with 0.85 friction, wiggles sideways, grows from 0.02 to 0.15 and
pops when it leaves a water source, stops rising or travels 3 blocks. Its
provider passes yAux as the Z speed, which is kept.
The particle providers harness gets water worlds for sulfur_bubbles,
bubble and bubble_column_up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify and CI parity pass, and the independent review is OK: 12 update tests; v0.2.0.1 ranks below every 0.2 tag; a 0.2 install is never offered a dated tag.
From 0.2 the game is tagged vX.Y.Z and the launcher [email protected] (docs/versioning.md). The site
took only vYYYY.MM.DD as a game release and launcher-v as a launcher, so it would keep offering
launcher 0.16 and showing the last dated patch notes.
src/versions.js holds the tag test and order (every 0.2 tag above every tag from before, semver
within, a pre-release before its release), shared by build.mjs and main.js; the download button
picks the newest launcher by tag, the notes the newest stable game releases, and the launcher
lookup falls back to launcher@ tags before launcher-v. Nothing shows "0.2" until a 0.2 release
exists. `npm test` runs its tests (node --test).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic decided (2026-10-21) that the next release is 0.2 for both the game and the launcher, with one
clear scheme instead of vYYYY.MM.DD(.N) and launcher-vX.Y.Z. This page is the design the other 0.2
pull requests implement:
- tags vX.Y.Z and [email protected] (launcher-v0.2.0 is taken), on a line in .github/release-line.txt;
betas stay pre-releases under their final tag; semver pre-release tags reserved for previews;
- one order for every reader: each 0.2 tag above every tag from before;
- how release.yml, launcher.yml and promote.yml map onto it (HORIZON_VERSION, HORIZON_LAUNCHER_TAG);
- what launchers and servers from before 0.2 do with it, and the bridges that move them
(launcher-v0.17.0, the last dated game release made latest before v0.2.0);
- the orchestrator's steps for the day Ludovic says go, freeze off included.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify and CI parity pass, and an independent review found no blocking issue. The release, launcher and promote workflows stay disabled with RELEASES_PAUSED=1. Without .github/release-line.txt the train decides exactly as before (checked against every existing tag).
🤖 Agent: merged. Verify and CI parity pass, and an independent review found no blocking issue. The release, launcher and promote workflows stay disabled with RELEASES_PAUSED=1. Without .github/release-line.txt the train decides exactly as before (checked against every existing tag).
🤖 Agent: merged. Verify passes. CI parity's only failure is world_spawn seed -2080727421395798566, where vanilla itself gives several heights across runs (also on main; see #1112). Screenshots in the body.
🤖 Agent: merged. Thank you @feeldev12! Verify passes. A 2-line fix to benchmark tooling: the world folder is created before the marker is written, so slow devices no longer race the async world open.
An ignored test reads vanilla's advancements and item tags from the
prepared resources (HORIZON_RESOURCES) and reports what an inventory
change and a recipe unlock test now. On the Mac mini (release):
inventory_changed has 1914 criteria, the index is built once in 0.75 ms
with the tree (off the tick), and a nine-stack inventory tests 67 of them
(found in 16 us); recipe_unlocked has 1739, an iron pickaxe tests 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerWaypointManager.removePlayer disconnects every waypoint the player
was receiving: a player who goes to another level gets an untrack for each
player it tracked in the one it left. Horizon's per-level managers tick
with the players in their own level only, so the untrack had no connection
to go to and the player kept the old level's markers.
Each level's manager now also gets the connections of the players in the
other levels, for these untracks.
The player-targets and dimension-players harnesses compare the waypoints
as each step leaves them (the last packet for each waypoint): arriving in a
level, vanilla sends the same waypoint up to three times within the step,
and the packet by packet order is test_locator_bar_parity's. Both pass:
123 and 40 lines identical.
Part of #843
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two main-thread costs from the hub tick (found on the companion branch with
HORIZON_SPIKES, commit 060c795e; its companion and metering parts stay there):
- Every inventory change tested all ~1,900 inventory_changed criteria, and
every recipe unlocked (an iron ingot unlocks dozens) all ~1,300
recipe_unlocked ones: 1.5-5 ms on such a tick. The tree now indexes
inventory_changed criteria by the items that can pass them (tags resolved;
the few without an item list are always tried) and recipe_unlocked
criteria by recipe, so a change tests the 20-40 that could match. Same
answers: a criterion with an item-limited predicate can only pass with one
of those items held.
- Each log line was an unbuffered stderr write plus a latest.log write under
a mutex on the calling thread; a full pipe or a slow disk held the tick.
The logger now formats the line and keeps it for crash reports on the
calling thread and hands the I/O to a "Log writer" thread; flush() waits
for it (crash report, exit).
(cherry picked from commit 060c795e, companion parts left out)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla keeps a ServerWaypointManager per level: a player in another level
is neither a transmitter nor a receiver there. Horizon passed every player
to one manager whatever their dimension, so a player who went to the
Nether stayed tracked at their Nether coordinates by the players left in
the Overworld (`track Bob block 0 100 0`), and nothing was re-tracked when
they came back.
Each level now has its own manager, ticked with the players in that level:
a player changing level leaves the old one (untracked for everyone there,
as ServerLevel.removePlayerImmediately) and joins the new one
(addDuringTeleport). The player-targets and dimension-players harnesses
now compare the waypoints too, instead of leaving them out.
Fixes#843
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's body is rebuilt when it respawns, and its hurt and attack logs
went with the old one: the control socket's hurtlog lost the hurts that
killed it whenever the death and the respawn fell between two reads.
The logs now pass to the new body, and a dead guest's are read too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit df9fa5f018)
Every hurt a player takes (PlayerMotion::hurt_blocking, which all player
damage goes through) is kept: the damage type, the amount before blocking
and armour, what a raised blocking item stopped, the health and absorption
lost, the armour and armour toughness it met, the fire ticks, the health
left and, for a mob's blow, the knockback it gave. Every melee blow on a
mob is kept too: the damage after the attack strength
(getAttackStrengthScale), the strength, a critical hit, the mob, whether it
landed and killed. The last 64 of each are kept.
`hurtlog` on the control socket hands them out, per player (the host and
each LAN guest), and clears them: a harness can read what a fight did hit
by hit and compare it with vanilla's numbers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit fdfa952379)
OminousItemSpawner.addAdditionalSaveData keeps its item and
spawn_item_after_ticks; Horizon saved neither, so an ominous trial
spawner's key or potion floating mid-wave vanished when the world closed
or its chunk unloaded. They are now written with the chunk's entities
(UUID and unmodelled fields kept), read back as live spawners whose tick
count starts again from 0 (Entity.tickCount isn't saved), and leave the
level with their chunk.
Fixes#462
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sleeping guest stood on top of its bed for everyone else: the server
sent its pose but never its bed (LivingEntity.SLEEPING_POS goes only to
the sleeper itself), Horizon's clients had no sleeping pose for other
players, and the host's own models of its guests ignored the bed. A
player paired with the sleeper later (the host walking up from afar)
saw the same.
The tracked state of a sleeping player (the host or a guest) now holds
its bed, so pairing and updates send it, and its removal sends the empty
position back; the clients and the host's guest models lie the player in
the bed as LivingEntityRenderer.setupRotations does for Pose.SLEEPING
(the villagers' sleeping pose, by the bed's facing).
lan_e2e `sleep`: the host draws the sleeping guest lying, the guest the
sleeping host, and neither once they are up.
Fixes#1045
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Like the dragon, a spike's crystal gets its id from the entity counter as
EndCrystal's constructor does, instead of only once a client tracks it: a
player the level serves without a client can then aim at it. The fight's
control status lists each crystal's id and position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit e32e5031c1)
The dragon read the window's own player for everything: network players
(LAN guests, players on a server) were never targeted, strafed, charged,
knocked back, bitten, breathed on or hit by its fireballs.
- It sees the level's players (the window's and every guest in the level)
through `TargetingConditions.forCombat` as each phase asks
(`Level.getNearestPlayer`): alive, neither creative nor spectator, none on
Peaceful. The holding pattern strafes the enemy nearest the egg, the
landing approach comes in opposite it, the perched dragon scans within 20
blocks (10 up or down) and charges within 150.
- The strafe keeps its own target (`attackTarget`) and goes back to the
holding pattern when it leaves the level.
- A destroyed crystal sends a holding dragon after the player who destroyed
it, else the nearest player (`EnderDragon.onCrystalDestroyed`), when it can
attack them.
- Wing knockback and the head and neck bites, the breath and fireball clouds
and its fireballs act on every player alike; what hits a guest runs as
that guest, its knockback sent to its client.
- The dragon takes its id and its parts' when it is created, so a guest can
attack them in a world without network players too.
A unit test covers the targeting rules (spectators and creative players
passed over, ranges, Peaceful, the nearest to the egg).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2eef4e4bf5)
Primed TNT (and falling blocks, projectiles) spawned from /summon get
their entity id only when first tracked, so the summon found no new id
and answered "Unable to summon entity" though the entity was there and
went off. The summon now also counts those lists, and names the type:
"Summoned new Primed TNT", as vanilla.
Test: a TNT summon in the command execution corpus (world group:
125/126 match vanilla; the one left, /waypoint list, was there before).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit dd733684bc)
The world spawn was only filled in when the world was first saved, from
the player's spawn point at that moment: a bed slept in before then became
the world spawn, and anything asking for the world spawn earlier (a
guest's respawn) fell back on the host's spawn point instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 03e621ed3a)
SetPotionFunction was skipped, so the potions a loot table gives came
out with no contents: a piglin's barter never held fire resistance (the
drinkable and splash ones, 16 of 469), only uncraftable potions. The
merchant and /loot code had it; the shared loot evaluator now does too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 1b2767122d)
- A perched dragon (scanning, attacking, flaming, hovering) shrugs arrows
off as `AbstractDragonSittingPhase.onHurt` does: no damage, and the arrow
is set alight for a second and turns back instead of being spent. Arrows
that hit a part or a crystal deal the arrow's damage with its Power bonus,
and turn back when the hit is refused (a dying dragon, an invulnerable
crystal).
- The damage cooldown is `LivingEntity.hurtServer`'s: within it a stronger
hit lands its excess over the last one instead of being dropped; the hurt
sound plays only for a new hit.
- Explosions hit as `ServerExplosion.hurtEntities` does: the dragon itself
(its body) and then each part, each by its distance and its exposure
(`getSeenPercent`, so cover protects it), through the cooldown; and every
end crystal in reach blows up, not only the first.
`Dragon::hurt` and `Dragon::explosion_hits` are pure, with unit tests for
the perched arrows, the cooldown's excess, the parts' quarter plus one,
the take-off after a quarter of its health, and exposure.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c730ed9ed8)
On a dedicated server (and single player's server thread) the End is ticked
with no local player, and `tick_fight` returned before spawning the dragon:
players in the End found crystals but never a dragon.
- The fight finds its players as `EnderDragonFight.updatePlayers` does, every
20 ticks: every player alive within 192 blocks of (0, 128, 0), the host and
the network players in the level alike. Scanning the state, the respawn
sequence, `createNewDragon` and the crystal count run while it has one;
the dragon and crystals tick whenever the End does.
- The dragon is tracked to network players (`minecraft:ender_dragon`, its
health and `DATA_PHASE` as vanilla pairs them, no attribute instances) and
reserves its eight part ids after its own, as `EnderDragon`'s constructor
does, so clients can attack the parts; a network player's attack on a part
or an end crystal is aimed at it.
tools/test_server_dragon_parity.py sends a lone vanilla-codec client to the
End of vanilla's dedicated server and of Horizon's: the fight starts and the
dragon is tracked with the same spawn data, synched data entries and
attributes. VanillaProtocolClient's entity mode answers a scene's teleport
with the acceptance alone, as the 26.3 client does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 618db927d9)
.github/release-line.txt = 0.2: the release train's next runs publish v0.2.0 (a beta, as always)
and [email protected] at once, "the first release of 0.2" (docs/versioning.md). Merge it only after
the bridges (the last dated game release, made stable, and launcher-v0.17.0) are out: the go-day
steps are in docs/versioning.md.
Also, because they say 0.2 is out: the site's FAQ explains the numbers, Install says an old launcher
updates twice, hosting.md gives the one command for a server that says it is up to date, and the
launcher's Cargo.toml says 0.2.0 so builds from source (packages) are on the 0.2 line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
release.yml builds every game release with HORIZON_VERSION, the tag without its v
(docs/versioning.md), as launcher.yml already does for the launcher: no version-bump commits.
- horizon_core::release::{RELEASE, VERSION}: the built-in version, or Cargo.toml's for a build from
source. (horizon_core::version stays Minecraft's, generated by gen_version_consts.py.)
- horizon-server and horizon-prepare --version, the server's /api/v1/status, horizon-ctl's MCP
serverInfo and the "Horizon/<version>" user agents use it; they all said 0.1.0.
- The client's crash reports and telemetry say v0.2.3 from the built-in version wherever the game
is installed; releases from before 0.2 (no version built in) still read the launcher's folder name.
- The launcher's benchmark telemetry used Cargo.toml's version instead of the release's.
- release.yml checks that horizon-prepare and horizon-server --version say the release's version.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify passes. Local integration of #1109–#1112 on main ae21bd54: parity --changed 142/142. After the rebase onto #1094: 101/101, with screenshots in the body.
🤖 Agent: merged. Verify passes. Local integration of #1109–#1112 on main ae21bd54: parity --changed 142/142. CI parity's world_spawn diff (seed -2080727421395798566) also happens on main: vanilla itself gives y64/65/66/72 across runs. The other failure is the known dedicated_server pearl flake.
🤖 Agent: merged. Verify passes. Local integration of #1109–#1112 on main ae21bd54: parity --changed 142/142, with before/after item counts in the body.
The station screens' hand-written quick move put a payment stack's first item
into the beacon, fell back to the other inventory half when an enchanting,
grindstone, stonecutter, cartography or anvil rule's own slots were full, and
sent every input back hotbar-last-first. Follow each menu's quickMoveStack
(EnchantmentMenu, BeaconMenu, GrindstoneMenu, StonecutterMenu,
CartographyTableMenu, ItemCombinerMenu with SmithingMenu's
canMoveIntoInputSlots), repeated as doClick's QUICK_MOVE does.
A local brewing stand's clicks now run on the verified menu engine, as a
server's already did, so its shift-clicks, drags (quick craft) and double
clicks are BrewingStandMenu's, which test_menu_click_parity covers.
Part of #275
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The menu style is one process-wide flag, and options tests switch it to lay
out a page in Horizon's or Classic's style. Running in parallel, one test's
switch moved another's widgets (classic_graphics_presets_set_the_distances
failed 4 of 200 runs of the options tests). Under cfg(test) the flag is a
thread local, Classic first; the game keeps the shared atomic.
Fixes#802
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The title box is an unbordered, centred EditBox (black, no shadow) in
vanilla. Horizon kept Ctrl/Cmd+A's selection but never drew it, and centred
the title together with its cursor. Draw it with ui::append_field (now with
a text shadow flag): the value centred alone, the cursor after it, and the
inverted highlight. horizon-ctl gets the super (Cmd) and F6 key names, and
A/C/V/X with the edit modifier held act on the open book.
Part of #275
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Classic Extras laid every setting out as a full-width button. Pair each
section's buttons as vanilla's OptionsList.addSmall does (the same layout as
Video Settings), and shorten "Lower sensitivity while zoomed" to "Slower
turning" so it fits a half-width button.
Horizon's Extras page shows a switch's own settings (Zoom key, Minimap size,
FPS corner...) only while that switch is on, so it stays one row per extra
as the list grows. Classic keeps every button, as vanilla screens do.
Fixes#812Fixes#815
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A world's session.lock is an flock held by the open file. Closing the file
releases it only once every copy of the descriptor is gone, and a process
spawned on another thread holds a copy until it execs. Under a parallel
cargo test (crash reports spawn uname/sw_vers), deleting a world right after
dropping its lock could then fail with "already open". Unlock explicitly
in Drop, as vanilla releases the FileLock before closing the channel.
Fixes#749
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A centred mono source played at full gain on each channel; OpenAL Soft
(vanilla's LWJGL build, HRTF off) renders stereo pairwise: 0.596 per side
ahead, 0.404 behind, 0.5 above or below. Explosions were 4.5 dB louder than
vanilla. Pan with the camera's forward and up vectors, play a mono output
unpanned, and release the limiter over 0.2 s like OpenAL Soft's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/versioning.md: with .github/release-line.txt (MAJOR.MINOR) the train tags game releases
vMAJOR.MINOR.PATCH and launcher releases [email protected], the patch after the newest
release on the line; the line's first release goes out at once (still not while RELEASES_PAUSED is
set). Without the file nothing changes: vYYYY.MM.DD[.N] and launcher-vX.Y.Z. The file is not added
here; the switch to 0.2 is a separate one-line pull request for the day Ludovic says go.
- tag_key: every 0.2 tag ranks above every dated or launcher-v tag; semver order within, a
pre-release before its release. release_channels.py uses the same key and tag test, so promotion
and "latest only moves forward" work across the switch.
- A line's first release says so in its notes; 0.2.0's explains the numbering and how a server
that says it is up to date takes it (horizon-server update --version v0.2.0).
- launcher.yml strips either prefix for HORIZON_LAUNCHER_VERSION and passes HORIZON_LAUNCHER_TAG,
which the launcher uses to know its own line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From 0.2 the game is tagged vX.Y.Z and the launcher [email protected] (docs/versioning.md). The launcher
compared every tag number by number, so it would never offer game v0.2.0 over v2026.10.19.15 nor
update itself to 0.2.0 from 0.16.0 (launcher-v0.2.0 is also taken by an old release).
- release_order(): tags from before 0.2 (vYYYY.MM.DD[.N], launcher-vX.Y.Z, bare versions) compare as
before; every 0.2 tag follows them, in semver's order (a pre-release before its release).
- own_tag(): the tag this launcher was released as. launcher.yml will pass HORIZON_LAUNCHER_TAG; a
release with only HORIZON_LAUNCHER_VERSION was a launcher-v one; a build from source is on 0.2.
So the last launcher-v release (the bridge old launchers can see) updates itself to launcher@.
- The matching-refs fallback looks for launcher@ tags first, then launcher-v.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From 0.2 Horizon's game releases are tagged vMAJOR.MINOR.PATCH (docs/versioning.md). newer() compared
tags number by number, so v0.2.0 looked older than v2026.10.19.15 and an installed server would never
offer or install it. Every semver tag now outranks every date tag; within semver, patch and minor
compare as numbers and a pre-release (v0.3.0-rc.1) comes before its release. Pruning old releases
sorts with the same order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify passes. Local integration of #1101–#1107 on main caeffffb: parity --changed 302/304, and the two failures (block_particles, entity_model) fail identically on plain main. CI parity's secure_chat failure on #1101 logged 'transcripts identical' (flaky).
🤖 Agent: merged. Verify passes. Local integration of #1101–#1107 on main caeffffb: parity --changed 302/304, and the two failures (block_particles, entity_model) fail identically on plain main. CI parity's secure_chat failure on #1101 logged 'transcripts identical' (flaky).
`open` replies once the world starts opening on a worker, so the save
folder may not exist yet when the benchmark writes horizon-benchmark.txt
into it. On a slower device (an Android phone) the write failed with
"No such file or directory" and the run ended before the first scene.
With the camera above the build height (or below the bottom), vanilla's
SectionOcclusionGraph.initializeQueueForFullUpdate seeds the search with
the top (bottom) layer of sections around the camera instead of the
camera's own section. Horizon started from the camera's section and could
only step one section past the world's edge, so from y 352 up (two
sections above the top) it reached nothing and every chunk vanished.
Fixes#1037
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify passes; CI parity passes except dedicated_server, which fails the same way on main (play-pearl teleported 0 vs 1, the known flake in FEATURES.md LATER).
🤖 Agent: merged. Verify passes; CI parity passes except dedicated_server, which fails the same way on main (play-pearl teleported 0 vs 1, the known flake in FEATURES.md LATER).
🤖 Agent: merged. Verify passes; CI parity passes except dedicated_server, which fails the same way on main (play-pearl teleported 0 vs 1, the known flake in FEATURES.md LATER).
Moving blocks are minecraft:moving_piston blocks with a block entity.
The client drew nothing for them and gave them no collision, so pistons
on a clock flickered and players walked through them (#935).
The world now keeps a copy of the level's moving-piston block entities.
SceneCollisions::moving_piston_shape returns their shape
(PistonMovingBlockEntity.getCollisionShape, already in piston_shape),
and game/moving_pistons.rs ports PistonHeadRenderer: the moved block at
its interpolated offset, the moving head's short state, and a retracting
piston's head and extended base.
Entity pushing (moveCollidedEntities) is not ported yet.
Fixes#935
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BoatRenderer draws its water_patch part with RenderTypes.waterMask (depth
only) unless the boat is under water, so translucent water behind it fails
the depth test inside the hull. Horizon loaded the part but never drew it:
the water surface showed through the boat. The patch now draws with a
depth-only Water mask pipeline after the entities and before translucent
terrain (rafts have none, as RaftRenderer).
Turning in a boat stepped the camera at the tick rate: positionRider adds
the boat's deltaRotation to the passenger's yRot after its yRotO was kept,
and LocalPlayer.getViewYRot interpolates a passenger's yaw between them.
The camera now spreads the turn over the tick's frames.
Fixes#1016
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The camera's biome alone gave the sky, fog and water fog colours, so they
jumped at every biome boundary (most visibly the Nether fog). Vanilla's
EnvironmentAttributeProbe weights the noise biomes of a 6x6x6 quart
neighbourhood around the camera with GaussianSampler's kernel and lerps
the spatially interpolated attributes between them
(SpatialAttributeInterpolator, ARGB.srgbLerp); unloaded chunks count as
plains.
Fixes#797
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A thrown splash or lingering potion or experience bottle was a stand-in
terrain particle spawned at its tick position: darkened by the terrain
particle colour, without the potion's colour, two overlapping copies and
no interpolation between ticks. Draw it as vanilla's ThrownItemRenderer
does: the item model (GROUND, with the potion tint) facing the camera,
interpolated, lit at its light probe, hidden for its first two ticks near
the camera (ThrowableProjectile.shouldRenderAtSqrDistance). Witch potions
draw as the splash potion of their effect instead of a bare bottle.
Fixes#818
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EndCrystal.tick advances the crystal's time wherever it is; only the fire
at its block needs a dragon fight. Horizon advanced it only during the End
fight, so a crystal placed elsewhere kept a fixed time and jumped back
every tick (it flickered instead of turning).
Fixes#1051
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Breaking, mining, landing and other block particles took the block's up
face and a fixed grass or foliage colour, so grass blocks crumbled as
tinted grass tops (moss-like). Vanilla's TerrainParticle uses the model's
particle material (first variant / first multipart selector) and tint
source 0's colorAsTerrainParticle: the biome-blended in-world colour,
none for grass blocks (BlockTintSources.grassBlock) or blocks without a
source. The in-wall screen overlay and block markers use the particle
material too.
Fixes#1034
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The glide flag is the server's: LivingEntity.updateFallFlying clears it when
canGlide fails (the player is on the ground), from the position the client
sent, and the client gets the change on its next tick. So in vanilla a glide
that lands goes on for one tick on the ground, with travelFallFlying's drag
instead of ground friction, and a jump on that tick (with its sprint boost)
keeps the speed: the elytra hop. Horizon ended the glide on the landing tick,
so each hop lost about half of the speed to ground friction.
Fixes#1056
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BeehiveDecorator stores 2 or 3 bees in the nest's block entity. World
generation kept them, but trees placed in the live level (a sapling grown next
to flowers, which picks the *_bees_005 trees, and /place feature) dropped them:
LevelFeatureWorld had no store_beehive_occupants, so the nest had no block
entity at all. Breaking it released nothing and bees could not enter it.
LevelAccess gets load_block_entity, the game loads the occupants into the
nest's block entity, and the occupant NBT is shared with world generation.
Fixes#1097
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.attack adds causeExtraKnockback after the hurt: the weapon's knockback
enchantment (halved) plus 0.5 for a sprinting full-strength hit, pushed along
the attacker's yaw (sin, -cos) rather than away from it, which lets a player
steer a hit target by turning while attacking (knockback displacement).
Horizon had only the enchantment part, on mobs; sprinting hits added nothing
and players (LAN PvP) got no extra knockback at all. Now:
- player hits on mobs and players: one extra knockback of that strength with
LivingEntity.knockback (knockback resistance, lift only from the ground),
the attack.knockback sound, and the attacker's sprint stops;
- the knockback a player takes (mob and PvP hits, dealDefaultKnockback with
0.4) reads its knockback resistance (netherite) and only lifts from the
ground: an airborne player kept being thrown up by every hit.
Fixes#504
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The player's ATTACK_DAMAGE and ATTACK_SPEED modifiers change in
LivingEntity.detectEquipmentUpdates, once a tick, while Player.attack reads
the weapon's enchantments and smash bonus from the stack in hand at the
attack. So swapping hotbar slots just before an attack hits with the old
item's damage and charge and the new item's enchantments (a Wind Burst or
Breach mace after another weapon): attribute swapping. Horizon read both from
the item in hand at the attack.
Also Player.tick's reset: switching to a different item now resets the attack
strength the damage uses (only the indicator's ticker reset before), for the
host and for LAN guests.
Fixes#505
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two Blast Protection pieces now cancel explosion knockback (#1049):
- ServerExplosion.hurtEntities scales knockback by EXPLOSION_KNOCKBACK_RESISTANCE
for living entities. The player's knockback ignored it, and mobs used
KNOCKBACK_RESISTANCE (iron golems were never pushed; vanilla pushes them,
every type's explosion resistance base is 0).
- EnchantmentAttributeEffect.idForSlot: the modifier id ends with the slot,
so the same enchantment on two armor pieces adds up. The id was
<id>.<level>, so the second piece replaced the first.
- hitPlayers skips creative players who are flying: no knockback for them.
Fixes#1049
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minecraft.handleKeybinds only calls startUseItem while the player is not
using an item: use clicks are consumed and held use doesn't repeat. Horizon
repeated held use every 4 ticks regardless, so with a shield raised in the off
hand, aiming at a block placed the main hand's block (or spawn egg, boat...).
Fixes#1050
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.updateFallFlying starts with Entity.checkFallDistanceAccumulation:
while gliding and falling slower than 0.5 blocks a tick, the fall distance is
capped at 1. Horizon kept accumulating it for the whole glide, so landing after
gliding down (or after climbing and gliding back) dealt the full drop's damage.
Fixes#1046
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify and CI parity pass on the rebased head (b591c9dc), with before/after frame times in the body (rain 561 → 759 FPS, snow 663 → 981 on the Mac mini).
Low leaves can hide every face of the trunk's bottom log from where the
walk stops ("no face of block ... is visible from here"). The scenario
now breaks inside the tree loop and moves on to the next tree, as it
already did for one it cannot reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- survival and ui asserted that the Create New World click itself
answered `playing`. Since 7f4193f8 the in-frame world is prepared off
the UI thread behind the title screen, so the click answers `title`
and both scenarios failed in the frame (main's default) every time.
They now wait for `loaded`, as the other scenarios do.
- The survival spawn is random within the spawn radius, on the first
full top face below the MOTION_BLOCKING height
(PlayerSpawnFinder.getLevelRespawnPos), so a tree's leaves too: from
there no tree was reachable within goto's three-block drops. The
scenario now drops the player to the ground under the leaves first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's spawn chunks load by ticket level, so the level loading screen's
chunk map fills in Chebyshev rings. While the loading screen is up,
Horizon now picks chunks ring by ring (nearest first inside a ring)
instead of by Euclidean distance, which grew a disc.
Fixes#770
Co-Authored-By: Claude Opus 5.5 <[email protected]>
check_xp makes the host a spectator, out of the orbs' way, and left it
one for the rest of the run. A spectator sinks (Player.travel keeps 0.6
of its vertical speed each tick, about 0.2 blocks from standing), so the
next check that made it survival again, swings, landed it inside the
floor with the guest teleported beside it: the guest's feet were in the
floor's stone, which hid the dirt it tills from every angle ("no face of
block ... is visible from here"). It failed only after the earlier
checks, never alone.
check_xp now ends in survival where the host stood, as check_trials
already does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemEntity.tick merges a dropped stack with mergeable items whose boxes
meet its own grown by half a block sideways, every 2 ticks while it moves
between blocks and every 40 otherwise (mergeWithNeighbours, tryToMerge,
merge). Horizon never merged, so fresh chunks, where water and falling
blocks break leaf litter, moss and plants, filled up with single items.
FallingBlockEntity.tick also places (or breaks) the block within the
entity's own tick. Horizon moved every falling block first and placed them
after, so a block falling right behind another landed in the same cell and
broke into an item: stacked sand and gravel in fresh chunks dropped.
Fixes#996
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle prints every ProtoChunk postprocessing position and chunk_dump
prints Horizon's, so a chunk that schedules different fluid or shape
updates after generation (the blocks that later tick and drop items in
fresh chunks, #996) fails the harness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify passes. The change runs only on the server thread (opt-in today), and the PR body's proof shows lan_e2e --server-thread going from 35/39 to 38/39. #1072's switch needs it.
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
SectionOcclusionGraph.runUpdates, both rules Horizon left out:
- a section leads on through whatever any face it was entered through
connects to (Node.sourceDirections), not only the first one;
- beyond 3 sections from the camera, a step is taken only if the line
from the section's corner towards the camera, in 28-block steps while
farther than 60 blocks, stays in sections the search has reached.
The search takes the camera position for the ray test. Exact equivalent
of vanilla's, faster: one ray per axis per node (both steps along an
axis share the corner; while the camera stays in the searched-from
section no point falls in a neighbour the loop adds), an answer per
section corner during a full search (neighbours share corners; reached
sections are only added, so a line that stayed in them still does, and
one that failed is checked again from the sample it failed at, stepping
over the earlier ones with the same additions), and an integer floor
instead of libm's on x86-64. Tests compare every cell with a literal
port of runUpdates (map of nodes, FIFO, JOML arithmetic), and a resumed
line with one checked from its corner.
Fixes#782
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WeatherEffectRenderer reads level.getHeight(MOTION_BLOCKING, x, z) for
every column within the weather radius. Horizon scanned each column block
by block from 10 blocks above the radius down to the ground, with a
string search per block, every frame: about half a millisecond of CPU per
frame in rain on a Ryzen, more on laptops (#899). Client chunks now keep
a MOTION_BLOCKING map next to their WORLD_SURFACE one, built on the first
read and dropped by writes that can move it, from the facts world
generation already uses (blocks_motion_in_heightmap or a fluid).
This is also vanilla's rule where the scan differed: slabs, stairs and
other motion-blocking blocks without a full collision box now stop rain,
and a roof more than 20 blocks above the camera does too.
Tiling a column at each texture repeat no longer allocates two vectors
per column.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
owner_console_command ran the window's commands as the world's owner with
the hub taken out, so a selector saw only the owner (`team join … @a` left
the guests out, and friendly fire still hurt) and `defaultgamemode` never
reached the hub (guests joined in the world's old mode). It now puts the
hub back while the command runs, as run_guest_commands does for a guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Title backdrop built on a worker (fixes#896). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
🤖 Agent: merged. Creative pick block in inventories (fixes#1048, Visivel). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
🤖 Agent: merged. Firework tooltips (fixes#1047, Visivel). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
🤖 Agent: merged. Cobwebs slow falling blocks (fixes#933). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
🤖 Agent: merged. Stars built once (fixes#1054, Visivel and rdbtCVS). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
🤖 Agent: merged. Blocks can't be placed into entities (fixes#1052, Visivel). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
🤖 Agent: merged. Shears carve pumpkins (fixes#1067, reported by Tydragon). Integration proof (main + #1079#1081#1082#1083#1085#1086#1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
- sprites: vanilla models name the missing texture on purpose (block/air.json's
particle is minecraft:missingno), so using it is not worth a warning (#853).
- audio: when the resources hold no sounds.json, the game plays nothing on a
working output and the log said only which device opened. It now says the
sounds were not copied and how to fix it (#1035, #1076).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player installed Java 25 but JAVA_HOME still named an older JDK, so
Prepare stopped with "Java 25 is needed", and the launcher showed that
error at the foot of the page, out of sight until the window was enlarged.
- horizon-prepare java::find: after JAVA_HOME, the PATH and macOS's
java_home, look in the folders JDK installers use (Program Files vendors
on Windows, /usr/lib/jvm on Linux, JavaVirtualMachines on macOS, ~/.jdks,
SDKMAN), JDKs named for the pinned major first.
- launcher: the preparation's progress and error sit right under the
buttons instead of below the rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client JAR ships an empty font/include/unifont.json; the real provider
list and font/unifont.zip, unifont_jp.zip and unifont_pua.zip (1.9 MB) come
only from the asset index, which preparation never copied. Every character
the bitmap fonts lack showed as a missing-glyph box (a server's "▸").
Full preparation now imports them after extracting the JAR, like the title
panorama, and `horizon-prepare fonts` repairs prepared resources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mojang republished the 26.3 version JSON again (sha1 61fff131), with a new
asset index 34 (0c8f43b2) that changes only 142 translation files. A launcher
keeps one indexes/34.json, so every install made since then, official
launcher or Prism, failed the "pinned asset index" check: no sounds and no
title panorama.
- research-lock.json: accept the new version JSON, and list the asset
indexes of every accepted version JSON (accepted_asset_index_sha1).
- horizon-prepare: an index the lock does not know is still used, with a
line in the log, since every object copied from it is checked against its
own SHA-1; the sounds now require minecraft/sounds.json in the index.
- tools/lab.py: the same for the dev tools.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Measured on an Intel HD 620 (#897): rain and snow cost about 3 ms of GPU
per frame there, most of it from the weather radius; FXAA about 0.8 ms;
cave culling saves about 0.6 ms; mipmaps cost nothing measurable and
clouds about 0.1 ms. A new Cost::Saves tag ("Saves time", in the success
colour) marks a setting that makes frames faster when on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The integrated and dedicated server loops paced every tick at the tick rate,
so /tick sprint ran at 20 tps and never reported, and the integrated server
recorded no tick times for /tick query. They now share
MinecraftServer.runServer's pacing: no wait while sprinting, the sprint's
report sent to the operators as the server's, and tick times recorded.
The hub broadcasts ClientboundTickingStepPacket when a step starts or stops
(ServerTickRateManager.updateStepTicks) and sends a joining player the
steps left, not 0.
The camera, hand, view bobbing and the player's own model use
Camera.getCameraEntityPartialTicks (getGameTimeDeltaPartialTick(true)), so
they stay smooth while the level is frozen instead of moving at 20 Hz.
Fixes#1038
Co-Authored-By: Claude Opus 5.5 <[email protected]>
start_backdrop built VanillaTerrain on the event loop, so the title stopped
drawing for the whole build: 2.1 s cold and 0.86 s warm on an M4 Pro (fast
profile), far longer on slow CPUs. The worker now builds it and finds the
spot; the title draws its panorama until the generator is installed.
Fixes#896
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockItem.canPlace now checks every entity whose blocksBuilding is set: mobs,
armor stands (not markers), boats, minecarts, end crystals, primed TNT and
falling blocks, not only players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Linux dedicated servers lower their world-generation and packet-writer threads behind the server thread; nothing changes elsewhere. verify passed; parity: every harness passes except dedicated_server, which is flaky on main with the same failure modes (7-run A/B against main in the last comment).
The middle button (keyPickItem) over an inventory or the creative screen no
longer clicks like the left button: with infinite materials it clones the
hovered stack at full size (CLONE) or, holding a stack, drags full stacks into
slots (quick craft type 2); otherwise it does nothing. Agents can middle-click
widgets (click ID middle), and creative slots are ui widgets now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rockets list their flight duration and each run of equal explosions (shape,
colours, fade colours, trail, twinkle); firework stars list their explosion.
Both honour TooltipDisplay. New harness: tools/test_firework_tooltip_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A falling block's move now runs vanilla's checkInsideBlocks along its path: a
cobweb it passes or lands in makes it stuck (0.25, 0.05, 0.25), which scales
its next move and zeroes its velocity, and resets its fall distance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Right-clicking a pumpkin with shears now carves it like vanilla: a carved
pumpkin facing the clicked side (the side facing the player on the top and
bottom), four pumpkin seeds from carve/pumpkin popped out of that side, the
carve sound, one point of wear, the shear game event and the used
statistic. A pumpkin carved on a golem base builds the golem, as
CarvedPumpkinBlock.onPlace does; the dispenser shares that placement.
use_item_on parity: pumpkin cases on any face with the player turned any
way (the oracle and fixture take an optional yaw and face).
Fixes#1067
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Verify passed. Integration proof (main + #1041#1043#1021#1020#1032#659#1029#1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
🤖 Agent: merged. Verify passed. Integration proof (main + #1041#1043#1021#1020#1032#659#1029#1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
🤖 Agent: merged. Verify passed. Integration proof (main + #1041#1043#1021#1020#1032#659#1029#1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
🤖 Agent: merged, thanks @rdbtCVS! World saves are spread over frames and keep the session lock until every write is durable (with my small clippy fix). Integration proof (main + #1041#1043#1021#1020#1032#659#1029#1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
🤖 Agent: merged, thanks @rdbtCVS! Local worlds are prepared on a worker; the loading screen keeps drawing. Integration proof (main + #1041#1043#1021#1020#1032#659#1029#1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
🤖 Agent: merged, thanks @ItsPotatoTime! The action bar, title, subtitle and boss bar names now resolve legacy § codes, like the sidebar since #1003 and like vanilla's StringDecomposer.
🤖 Agent: merged. Every merge condition holds: the latest stable game release (v2026.10.19.15) and launcher release (launcher-v0.16.0) verify against the release key, legacy_releases.py --check passes, and launcher and horizon-server built from this branch install signed, pinned-legacy and beta releases and refuse an edited SHA256SUMS, an altered archive and a missing signature. verify passed; parity proof in the last comment.
Lithium, FerriteCore, ImmediatelyFast, Gnetum, ScalableLux, Structure
Layout Optimizer, AsyncParticles and Ixeris: per technique, the vanilla
cost it targets, whether Horizon already avoids it, and whether it is
worth measuring. From public descriptions only; no mod code read.
Fixes#991
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged inert. Without the MACOS_SIGN_IDENTITY variable (not set: Ludovic's Developer ID and notary setup don't exist yet), every new signing/notarizing step is skipped and the release, launcher, sign and promote pipelines are unchanged; verify and parity passed on this head, a launcher dry run passed with signing skipped, and today's ad hoc launcher bundle passes the new self-update signature check. Setting it up is in FEATURES.md LATER (4b).
Servers send § codes inside a component's text. Vanilla's font applies them to every literal, but the action bar, title, subtitle and boss bar names drew the component raw, so the codes showed as glyphs. Resolve them before measuring and drawing, as the sidebar already does.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: merged, thanks @ItsPotatoTime! Prefers an 8-bit non-sRGB surface (Bgra8Unorm/Rgba8Unorm) instead of RADV's fp16 ExtendedSrgb one, so colours are no longer washed out on Wayland. Mac and Windows already got Bgra8Unorm. Verify passed; parity doesn't cover surface formats.
SkyRenderer.buildStars builds the 1500 star quads once into a static
buffer; Horizon regenerated them on the CPU and uploaded about 200 KB of
vertices every frame while stars were out, which made nights cost more
than days (#1054). The stars now live in a static vertex buffer built
when the renderer starts, drawn after the sun and moon with one small
uniform: the view projection times the sky's rotation, and the star
brightness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rgba16Float is the first non-sRGB format RADV advertises on Wayland, and wgpu gives it the linear ExtendedSrgb color space. The renderer writes gamma-encoded values, so the compositor read them as linear light and washed every colour out.
Prefer Bgra8Unorm or Rgba8Unorm and keep the old choice as the fallback. A screenshot on a surface that is not 32-bit now reports an error instead of copying with an 8-bit row stride, which aborted wgpu validation.
Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: merged. Docs only (README and website/docs): the Servers page is in launcher 0.15+, and releases go from beta to stable. Verify passed; parity doesn't cover docs. This also redeploys playhorizon.dev, which still showed the 10-19 14:29 build.
horizon_core::VANILLA_VERSION is a re-export, which left the datafix harness's
PARITY_RUST_SCOPE open: its six shards reran on any Rust change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lab.VERSION, lab.GAME (.local/minecraft/<version>) and lab.RESOURCES
(.local/horizon-client/<version>) come from the lock, and lab.version_info()
reads the pinned client jar's version.json. Every harness, generator and
tool that named 26.3 (resource folders, jar and source paths, result
records, the vanilla client's --version and asset index, the bare login
client's protocol, the benchmark's options.txt data version, the packet id
table's header) uses them, so a candidate worktree pinned to another version
runs them unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/gen_version_consts.py writes what the pinned client jar's version.json
states (name, id, protocol, data version, pack formats, Java, stable, build
time) into horizon_core::version, and every Rust site reads it from there:
PROTOCOL_VERSION, GAME_VERSION, DATA_VERSION, the save's and the datafixer's
version name, /version, the crash report, the legacy ping, the dedicated
server's start line, the title screens, the default resource folder, the
resource pack format, and the launcher's and horizon-ctl's fallbacks. CI
checks the file against the pinned jar.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- crates/horizon-release-sign/legacy-releases.txt pins the SHA-256 of every archive of the 131 releases published
before signing (GitHub's upload digests, written by tools/ci/legacy_releases.py). The launcher and
horizon-server update install those releases only with exactly those files, so rolling back to one works and
nobody can swap its files later; every other release needs its signed SHA256SUMS.
- the_launcher_ships_with_a_release_key and horizon_server_ships_with_a_release_key fail while
release-keys.txt lists no key, so this can't ship refusing everything.
- Test tags moved to v2099.* so they never collide with a pinned release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher (game installs and its own updates) and `horizon-server update` fetch a release's
SHA256SUMS and SHA256SUMS.sig, check the signature against the keys built in from
crates/horizon-release-sign/release-keys.txt and the archive against the signed SHA-256, and
refuse anything unsigned, signed for another tag or by another key, or that doesn't match, with a
message saying why. Merge only once release-keys.txt lists the key and a signed stable release
exists: until then every download is refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the repository variable MACOS_SIGN_IDENTITY set, the launcher's Horizon.app and .dmg and the
game's three programs are signed with the hardened runtime and a timestamp, notarized and (app,
dmg) stapled, from a keychain of the Mac mini's own that keychain.sh unlocks with its password from
1Password. Unset, everything is signed ad hoc as before. The launcher checks an updated bundle's
signature before and after swapping it in, and puts the old one back when it doesn't hold.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged on verify. Test-only (a Python test fixture and a temp-folder name inside a #[test] fn), so no harness can see it; it turns main's verify green again after #235 and removes a horizon-server test flake.
a_stopped_server_saves_and_keeps_comments (settings.rs) and
writes_properties_and_syncs_back (config.rs) both used
horizon-server-config-<pid>, so when they ran at the same time one
removed or rewrote the other's horizon.toml and
writes_properties_and_syncs_back failed now and then. The settings test
now has its own folder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#235 made Game::capture read the surface size, warp the pointer to the
window's centre and set the HUD cursor, so the fixture's copy of the method
no longer compiled and verify failed on main. The fixture now stubs
winit's PhysicalPosition/PhysicalSize, Window::inner_size and
set_cursor_position, HudBatch and Game's renderer and cursor, and checks
the recentring: one warp on grab, two on release, the HUD cursor at the
centre, and nothing when releasing an already released pointer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged. Recentres the cursor like vanilla's MouseHandler.grabMouse/releaseMouse. CI verify and parity passed; Ludovic asked on 10-21 to clear the PR queue autonomously. If the cursor misbehaves on a Wayland compositor, file it and we'll follow up.
🤖 Agent: merged. Reviewed the storage host: keys are one [a-z0-9_.-] segment, ids are [a-z0-9_], and the quota, key cap and per-call write budget all hold. Thanks @Yvonne-Aizawa!
🤖 Agent: merged. pr_ready now checks the touched crates and their dependents instead of the whole workspace; CI keeps its workspace-wide checks. The earlier verify failure was an unrelated supervisor test. Thanks @rdbtCVS!
🤖 Agent: merged after verify and CI parity passed (modpacks-11: Chipped's 11 vines carried as vines; the 11 blockstate errors are gone from the game log).
- Reject a second running mod with an id already in use: two mods
sharing an id would also share capability storage's data_dir,
letting one read, overwrite or delete the other's state.
- Cap storage to 64 keys per mod, not just 4 MiB: zero- or near-zero-
byte values could otherwise flood the directory with files the
byte quota never sees.
- Add a 1 MiB per-call write budget, separate from the 4 MiB on-disk
quota: a loop replacing one key stays under the quota on every
check (the total on disk never grows) while still doing unbounded
synchronous disk I/O per call, which fuel never meters.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
A mod with capability storage gets get(key)/set(key, value)/delete(key),
backed by real files beside horizon-mods/ (.horizon-mod-data/<id>/, 4 MiB
quota per mod). Keys are a single path segment so they can't leave the
mod's own directory. Unlike in-memory mod state, this survives a
cargo-horizon-mod dev reload, since it's keyed by the mod's path and id
rather than its Store.
🤖 Agent: merged after verify passed and a local check on main + #988: chunk_generation, command_exec and mob_ai (the three harnesses that failed in CI on its old head, under load) all pass; modpacks-10's integration ran 301/301. Formations' structures generate from the noise column.
WaterlogWhenReplacingWaterProcessor (16 lists: the bottom pillars of its
villages and outposts) waterlogs a block that can be where the world holds
water, makes it dry where it does not, and gives a block placed in water a
block tick. Carry used to drop it; it is now the game's
horizon:waterlog_in_world_water, in the chunk being generated as the mod's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. Every release goes out as a beta and is promoted to stable after 6 hours if its smoke test passed and no release-blocker issue is open; launcher-v0.14.0 (stable by default) is already out.
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. CODEOWNERS for the release pipeline and update code. The protection itself (tools/ci/protect_pipeline.sh --apply) waits for the switch to the GitHub App.
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. docs/security.md: the security model in plain words, with what is live and what still waits.
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. Release builds on the Mac mini start from a clean checkout.
🤖 Agent: merged after verify passed (docs and website only; the site build fails on broken links, anchors or images). The README is a shorter front page that links to the new docs.
🤖 Agent: merged after verify passed (docs and website only; the site build fails on broken links, anchors or images). The website gets a Docs section with 11 pages; /server redirects to /docs/hosting.
One-paragraph pitch, download and install, what works today with links to the
site's documentation, hosting and modpacks in two lines each, then build from
source, contributing and the project status table (moved lower, unchanged in
form so the site and tests/test_parity.py still read it). The stale release
notes section and launcher version are gone: the release notes are linked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/docs has a left navigation and eleven pages written for players first, then
hosters, then contributors: what Horizon is, install, settings, multiplayer,
logs and bug reports, hosting (the launcher's Servers page and the command
line), modpacks, mods and data packs, performance, contributing and the FAQ.
/server redirects to /docs/hosting. The build checks every link between the
pages and every image.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed (74 + 9 + 1 launcher tests; launcher-only). The Servers page: create, start/stop, players, settings, backups, updates, run at login, and Friends over the internet with Tailscale (device sharing, an optional rule limiting shared friends to the game port, never Funnel). Ludovic's real-tailnet check is described in the PR.
🤖 Agent: merged after verify passed (launcher-only; no parity harness depends on it). The launcher's server backend and `horizon-launcher servers …` CLI.
🤖 Agent: merged after verify and CI parity passed (40 server tests; the whole create → start → join → whitelist → backup → restore → update → stop flow ran on the Mac mini and the ThinkPad). Adds `horizon-server config`, `backups --json`, `update --check --json`; fixes whitelisting on offline servers and updates as root in containers.
repurposed_structures:structure_void_processor returns null for a
template's structure_void blocks, which is what vanilla's block_ignore
does with blocks: [structure_void]. Carry writes it that way, so the
82 lists that use it (its mineshafts, ocean village and temples, the
jungle pyramid's hidden room, swamp village) no longer place structure
voids over the terrain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CloseOffAirSourcesProcessor walls off the air and the other liquids beside
a template's fluids, so its ocean mineshafts keep their water in. The game
runs it as horizon:close_off_air_sources (the same weighted blocks and the
mod's random, neighbours written in their sections as the mod does), and
carry maps the mod's 4 uses to it. The weighted block list is shared with
the fluid fix.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod's file for a vanilla, convention or other mod's tag adds its values in
load order; one with "replace": true now drops vanilla's entries and the
earlier mods' first, as a higher data pack does, and the log says which mod
replaced what (kept with the cached root, so a reused root warns too).
Carry keeps such tags instead of setting them aside: Towns and Towers'
seven minecraft:has_structure biome tags load. Mods still own no other
minecraft content.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
26.3's EnterBlockTrigger reads a block list from 'blocks' and skips
1.21.1's 'block', so a carried criterion matched any block the player
entered: Regions Unexplored's Downer, From the Tree Tops and Light as a
Frog were granted on the first dive into the sea. Carry renames the
field (20 advancements in Regions Unexplored, with its boat recipes'
in-water criteria).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without the oracle, a block's template is guessed from its blockstate's
property names, and a vine's (east, north, south, up, west) fit
cobblestone_wall. The game then refused Chipped's 11 vine blockstates:
a wall's sides are none, low or tall, so 'true' is an unknown multipart
property value. block_template looks at the sides' values and makes those
vines; Luminax's 32 walls stay walls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and a local check on main + #983 + #984 (clippy clean; chunk_generation and locator_bar, the two harnesses that failed in CI on the old head, both pass; modpacks-10's integration ran 301/301). Structures built under the sea are flooded and waterlogged; Better Ocean Monuments' processors carried.
🤖 Agent: merged after verify passed and a local check on main + #983 + #984 (clippy clean; chunk_generation and locator_bar, the two harnesses that failed in CI on the old head, both pass; modpacks-10's integration ran 301/301). Structures built under the sea are flooded and waterlogged; Better Ocean Monuments' processors carried.
The relative-dimension check teleported the player from the Nether's
80 100 -40 to the overworld's 644 100 -316, fresh ocean chunks at seed 1.
On some runs vanilla broke some of their kelp and seagrass while they
loaded (its chunk loading is asynchronous), which rolled those blocks'
loot tables, so /random reset * later counted 12 or 13 sequences
against Horizon's 11. 1 230 0 in the Nether lands at 12 230 4, terrain
loaded since the start: the scaling is still checked, and 10 runs in a
row pass (5 of the 16 runs before failed this way).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A table of what is live, waiting to merge, or waiting for Ludovic, so the page doesn't read as if it all were.
- Forks: their checks need a maintainer's approval, and run-parity runs them on the Mac mini; the Mac mini
section no longer says forks don't run there, and says what a clean release checkout does and doesn't fix.
- Blockers hold every beta while open; releases from before signing stay installable with pinned files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What an attacker could do before (instructions hidden in reports, a skipped check that counted as
passed, releases straight to every player, downloads trusting GitHub alone, pipeline files changed
like any other), what stops it now (untrusted text, verify always running, beta then stable,
signed updates, code owners, the agents' own GitHub App), what still needs Ludovic, and what it
doesn't solve. Written for the website's Contributing page; CONTRIBUTING.md links it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its work folder is shared with pull requests' parity jobs; with clean: false, whatever they left (target/, an
untracked .cargo/config.toml, a build script's output) could end up in a release. release.yml and launcher.yml
now always check out clean; target/ is rebuilt from scratch, and vanilla-data.sh relinks .local/ from its
preparations outside the checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- backend.rs downloads, installs and self-updates; links.json says where from; launcher/Cargo.toml and
crates/horizon-server/Cargo.toml pick the crates that check downloads.
- protect_pipeline.sh: ludovic111 is the only owner and can't approve his own pull requests, so he is the one
bypass_pull_request_allowances user. enforce_admins stays on, so verify is still required for everyone.
--apply reads the protection back and fails if GitHub didn't keep it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
.github/, tools/ci/, the release tools, the signing crate and the code that checks updates are
Ludovic's to review. tools/ci/protect_pipeline.sh prints (and with --apply sets) main's protection
with code owners' reviews required and no other approvals, keeping the required verify check;
it is for the orchestrator to apply once agents work as the GitHub App.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. The secret was set first (RELEASE_SIGNING_KEY in the release environment), so the next release is signed with this key.
- A release-blocker issue's text is never read: its author can edit it after a maintainer labels it, so an
open blocker holds every beta, and one closed as completed still holds the betas published before it closed.
- Even --force never promotes a beta that isn't newer than the stable release: horizon-server update and
install.sh download GitHub's latest.
- When the oldest beta newer than the stable release has waited 24 hours, promote.yml opens a "Promotion to
stable has stalled" issue with the last reason; the next promotion closes it.
- promote.yml: persist-credentials: false; issues: write for the stall issue.
- Website: the newest 100 releases, plus GitHub's latest (the stable one) when a day of betas pushes it out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game releases go out as betas (GitHub pre-releases, never "latest") with the Mac mini's smoke
report attached. promote.yml (every 30 minutes, or by hand from a phone) runs
tools/release_channels.py, which makes the newest beta stable and latest once it has been out 6
hours, passed its smoke test and no release-blocker issue covers it, with notes rewritten to cover
everything since the last stable release. release_train.py counts merges from the newest release of
either channel, and a beta's notes say it is one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The private key was generated by tools/ci/new_release_key.sh straight into
the release environment's RELEASE_SIGNING_KEY secret (never on disk or
printed); from now on every release's SHA256SUMS is signed with it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
As root in a user namespace (an unprivileged LXC or rootless container), GNU tar
keeps the archive's owners by default and fails on IDs it can't map, so the update
stopped at "tar couldn't unpack". Found driving the launcher's Update now from one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, plus modpacks-10's local integration (parity 301/301, structure-start oracle 150/150 and 134/134). Towns and Towers' namespaces carried in its manifest.
🤖 Agent: merged after verify and CI parity passed, plus modpacks-10's local integration (all nine branches on main: parity 301/301, structure-start oracle 150/150 and 134/134). Content mods may own namespaces besides their id; two mods may not share one.
🤖 Agent: merged after verify and CI parity passed, plus modpacks-10's local integration (all nine branches on main: parity 301/301, structure-start oracle 150/150 and 134/134). Mod loot tables from every namespace, so modded structure chests are filled.
The render thread summarized every observed chunk (256 columns, seven
light reads per block) and gathered each mesh job's inputs (neighbour
edges, visited columns, cloned spans) itself, within a budget of about
1 ms and 2 ms a frame but at least one chunk or tile: a large tile or a
chunk on a slow machine still cost tens of milliseconds in one frame.
Both now run on the far-terrain worker. Lod::observe hands the chunk
over (the latest per column, queued once), and the worker returns its
summary with its reductions (Done::Observed): an unchanged one is
dropped, a new one is kept and applied as a reduction was. A mesh job
takes a snapshot of the active tiles, their samples and the visited
summaries (Arc maps, cheap to clone) and gathers every tile's inputs
on the worker, as it did before inputs moved to the render thread.
At most OBSERVE_AHEAD resident chunks wait at once, since an edit
copies a chunk the worker still holds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1.21.1 read a {min, max} without a type as a uniform number provider; 26.3
dispatches on type alone. The game read such rolls and counts as 0, so
chests of 304 carried ATM10 tables (When Dungeons Arise 140, Repurposed
Structures 50, Structory and its towers 78, ...) came out empty or short.
Carry now types them in pools (rolls, bonus_rolls) and in the functions
that take one, nested bounds too; ranges such as limit_count's and item
predicates' counts stay ranges.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
ring writes Ed25519 keys as PKCS#8 v2 (with the public key), which Ubuntu's OpenSSL 3.0 refuses
("unsupported"); verify caught it on ubuntu-latest. keygen now writes v1, the 32-byte seed alone,
checked to give the same public key as ring's document.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
The game resolves names as vanilla does, Mojang first, so on an offline server a name someone owns got that
account's UUID and the player, who joins under the offline UUID, was refused. horizon-server now writes the
offline entry and has the game reload the whitelist.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Any repository code running earlier in the job that holds the key could reach it (a build patched
through Cargo.toml, or a step writing $GITHUB_ENV or $GITHUB_PATH for the next one). The `sign`
job checks out only tools/ci/sign_release.sh, which signs with the runner's OpenSSL; the publish
job, without the key, checks the result with horizon-release-sign as launchers will.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game releases now go out as betas and become stable 6 hours later (promote.yml). The launcher
installs new versions from the stable channel unless "Beta builds" is on in Settings; turning it
on checks again at once. The Versions page, `versions` and the MCP server show each build's
channel. The website's patch notes list stable releases only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
crates/horizon-release-sign signs a release's checksums with an Ed25519 key that exists only as a
secret of the `release` environment, and checks them as launchers will, against the public keys
built in from release-keys.txt. Dry runs sign with a throwaway key. Until release-keys.txt lists a
key, releases keep going out with SHA256SUMS alone and a warning; once it does, a missing or
unknown key fails the release before anything is published. tools/ci/new_release_key.sh pipes a
new key straight into the secret and prints only the public key. ring and base64 are already in
the lockfile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Labels started `checks` runs whose verify job was skipped, and GitHub counts a skipped required
check as passed: auto-merge merged #823 and #837 before the real verify finished. Labels no longer
start `checks` (the fork-only run-parity label has its own workflow), and verify has no condition.
The parity job moves into a reusable workflow both call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and local parity (perf-server-12: `tools/parity.py --changed origin/main` 98/98 with the three branches together, sculk / natural_spawn / spawn_rules harnesses pass; before/after numbers in the PR). CI parity was still queued on a busy runner.
🤖 Agent: merged after verify passed and local parity (perf-server-12: `tools/parity.py --changed origin/main` 98/98 with the three branches together, sculk / natural_spawn / spawn_rules harnesses pass; before/after numbers in the PR). CI parity was still queued on a busy runner.
horizon_launcher::servers keeps the list of servers and drives horizon-server
for them: create (init, then config set), start, stop, backups, settings, the
service, and the running supervisor's control API for the live status, the log
and console commands. The programs come from an installed, prepared version,
laid out as install.sh does (server-runtime/releases, current, game-files), so
horizon-server update and its rollback work on them unchanged; the game files
are a copy of the player's own prepared files.
Problems come back in plain words with the fix to offer (a busy port with a
free one, missing game files with Prepare, the EULA).
horizon-launcher servers list|create|set-up|run|forget does the same from a
terminal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`config [--json]` prints the settings; `config set KEY=VALUE...` writes them through
toml_edit, checked as server.toml is checked (nothing is written if one is wrong). A
running server takes the difficulty, the default game mode and the whitelist at once
through its console, and the backup settings because the supervisor reads them each
time; the rest waits for the next start, and the command says which.
Also `backups --json` and `update --check --json`, for the launcher's Servers page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed; cloud_render, environment_render, weather_render and 923 client tests pass, with before/after screenshots of Complementary Unbound. Java shader packs get real eye brightness, custom uniforms, Iris's sun path and tilt, and clouds=off/sun/moon flags. Part of #944 (reported by brabus_); shadows and gbuffers still to come.
🤖 Agent: merged after the verify rerun passed (the first run's failure was a flake; horizon-server tests pass 39/39 locally) and CI parity passed. Old releases are pruned automatically after an update.
🤖 Agent: merged after verify and CI parity passed (modpacks-10, reviewed with local harness proof in the agent's report). Carries Formations' structures.
🤖 Agent: merged after verify and CI parity passed (modpacks-10, reviewed with local harness proof in the agent's report). Carries the mods' pillar and fluid-fix processors in ported packs.
🤖 Agent: merged after verify and CI parity passed (modpacks-10, reviewed with local harness proof in the agent's report). Structure pillars and fluid fixes placed outside the template, like the mods do.
🤖 Agent: merged after verify passed and the HUD harness with new §-coded cases (34/220 on main → all pass). The sidebar applies § formatting codes like vanilla. Fixes#937.
🤖 Agent: merged after verify passed and local proof (menu_click fails on main 7978/8000, passes with the fix; place_recipe passes). The player's armour equip sound and event follow every vanilla path. Fixes#986 (reported by famescare).
Complementary Unbound drew clouds and light shafts inside terrain and a
second sun because the full-screen passes got wrong inputs:
- eyeBrightness was a constant (0, 240), so packs saw open sky
underground. It is now the block and sky light of the camera entity's
eye block (CommonUniforms.getEyeBrightness), and eyeBrightnessSmooth
follows it with the pack's eyeBrightnessHalflife (SmoothedFloat).
- Custom uniforms (uniform.* / variable.* in shaders.properties) were all
0: Complementary's eyeBrightnessM, isEyeInCave, starter, framemod8 and
the rest. horizon-shaderpack's uniforms.rs parses and evaluates them
each frame like Iris (stareval priorities, IrisFunctions, SmoothFloat).
- sunPosition, moonPosition, shadowLightPosition, sunAngle and
shadowAngle now follow CelestialUniforms: the sky's sun and moon angles
turned by Y -90, the pack's sunPathRotation about Z, then the angle. The
old sun vector pointed north-south instead of east-west.
- While a Java pack runs, sunPathRotation tilts Horizon's sun, moon and
stars as Iris tilts vanilla's, and clouds = off | fast | fancy, sun and
moon = false apply. Complementary and Sildur's set clouds = off; the
vanilla clouds Horizon drew anyway wrote depth the packs' light shafts
and fog took for terrain.
Fixes#944
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged, thanks Online5277! Ludovic decided: Classic menus by default for fresh profiles (saved choices and HORIZON_MENU_STYLE still win). We merged current main in; fmt, clippy -D warnings and 1,022 client tests pass, and verify passes. Fixes#548.
VanillaPlaceRecipeOracle prints VanillaMenuOracle.state, so the equips
field the menu_click harness compares broke its lines. Only the
menu_click oracle's own lines carry it now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.onEquipItem now runs for the player wherever vanilla calls
it: the inventory menu's ArmorSlot and off-hand slot (setByPlayer, in the
menu engine, so clicks, shift-clicks, number keys, drags and LAN guests'
clicks), right-click equipping (Equippable.swapWithEquipmentSlot), the
creative screen (every changed slot, as handleSetCreativeModeSlot sets it),
the swap-hands key, dispensers (now also a shield into the off hand) and
/item and /loot on armour slots. It plays Equippable.equipSound (the
generic armour sound by default) on PLAYERS when the item lands in its own
slot, and emits equip/unequip game events for armour; nothing for
spectators, on a client, or when the item is unchanged.
The menu click harness's oracle now records vanilla's onEquipItem calls.
Fixes#986
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's Font draws every literal through StringDecomposer.iterateFormatted:
a § code styles what follows and is not drawn, a lone trailing § is
dropped. The sidebar drew owner names, team affixes, scores and the title
raw, so servers' sidebars showed § and the code letters. They now go
through Text::resolve_legacy, as the tab list already does.
The HUD harness's sidebar cases gain legacy-coded owners, affixes, titles
and fixed score formats.
Fixes#937
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. Remote players' projectiles record their owner: ender pearls teleport and hurt the thrower, mobs target the shooter; docs/server.md row updated; part of #484.
🤖 Agent: merged after verify passed. A remote player's item used on a block is used once (a snowball thrown at the ground flew 2× main hand / 3× off hand); part of #484.
🤖 Agent: merged after verify passed. Chat commands from a dead player run, as in vanilla's handleChatCommand (fixes#973); dedicated_server transcript 604/604 lines identical.
A projectile a guest shot was recorded as the local player's (Who::Player), which on a
server means the host, or nobody on a dedicated server. So a guest's ender pearl
teleported the host on a LAN world and did nothing on a dedicated server, what it hit
answered to the host, and clients were told the projectile had no owner.
Projectiles now remember the guest who shot them (Projectile.guest, set when the
guest's turn ends). Clients get the guest as the owner (Projectile.getAddEntityPacket),
and the hit runs as that guest: their pearl teleports and hurts them, and the mob hit
answers to them. ThrownEnderpearl's endermite also needs monsters to spawn, and comes
where the player was.
The dedicated-server harness checks that the snowballs' owner is the player and throws
an ender pearl at the player's feet: vanilla teleports it and takes 5 health, main did
neither.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A vanilla client sends a right click as one packet per hand and part
(Minecraft.startUseItem): UseItemOn for the block's part and ItemStack.useOn, then
UseItem for Item.use when that passed, the main hand first. The server ran the whole
click, both hands, for UseItemOn, then the item again for UseItem: a snowball thrown
at the ground flew twice, three times from the off hand.
Each packet now does its own part with its own hand
(ServerGamePacketListenerImpl.handleUseItemOn / handleUseItem): UseItemOn stops at
useOn (no throwing, drinking, eating, raising, equipping, buckets or an ender eye off a
frame), and neither touches entities (Interact does). Horizon's own client, which
counted on the server trying the off hand, now sends the off hand's packets when the
main hand's use passes, as vanilla's does. UseItem's rotation is the player's now
(absSnapRotationTo) without sending the client a teleport back.
The dedicated-server harness right-clicks the ground with a snowball in the main hand,
then in the off hand, and counts the snowballs against vanilla's (2 and 3 on main,
1 and 1 now).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player attacks between the host and guests, or two guests, only checked the pvp game
rule. ServerPlayer.canHarmPlayer also asks the victim's team (Player.canHarmPlayer):
an ally on a team with friendlyFire false takes no damage, and the attacker hears the
no-damage sound.
lan_e2e's pvp check puts both players on such a team and checks that neither hurts
the other.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dead guest's packets were all dropped but the respawn request, so a command typed on
the death screen never ran. ServerGamePacketListenerImpl.handleChatCommand and
handleSignedChatCommand don't look at the player's health: the commands now run.
The dedicated-server harness sends `/trigger nope` from the death screen and compares
the reply with vanilla's before respawning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An install kept every release it ever unpacked in releases/. Once the game is up on the
new release (or a stopped server is switched), the update now deletes the old ones,
keeping the release in use, the one `previous` names (rollback's target), the one the
running horizon-server came from, and the `updates.keep` newest of the others (1 by
default, a new server.toml setting). A release that can't be deleted is reported and
left for the next update.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. If the game crashes natively after its Vulkan adapter line and before its first frame, the launcher switches the Graphics API setting from Auto to DX12 and tells the player; it doesn't relaunch by itself.
🤖 Agent: merged after verify passed. DirectX 12 for Windows: Auto tries Vulkan first and falls back to DX12 when Vulkan has no adapter or its device request fails; Video Settings > Graphics API (Windows only) and HORIZON_GRAPHICS_API override it. We merged main in (kept #771's LOD row and lodDistance next to graphicsApi); clippy -D warnings and 1,021 client tests pass, including the WGSL→HLSL translation of every shader. Not yet run on real Windows hardware.
- Integrated GPUs other than Apple's draw far terrain to 128 chunks at
most, whatever LOD Distance says; its description says so, and the
status reports the distance in use (`lod.effective_distance`).
- On machines with four threads or fewer, far terrain starts no detail and
one sample job per 500 ms while the near terrain loads, so the near
chunks and the frame get the cores first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Upload far meshes within about 2 ms a frame (at least one); the rest
wait, with their removals and the cutout, and apply in order.
- Gather mesh inputs across frames within about 2 ms, from a snapshot of
the active tiles' samples, instead of hundreds at once.
- Select tiles only when the plan, samples, covered columns or dirty tiles
changed; add the covered stand-ins only after a replan.
- Find the tiles touching an invalidated chunk per level, not by scanning
every active tile; share tile samples with mesh jobs (Arc) instead of
cloning them.
- Recompute the full-detail columns only when their completeness, the
center or the radius changed.
- Observe chunks for the far summaries within about 1 ms a frame (at least
one), unloaded ones first, with each block state's kind read once instead
of matching block names per block.
LODs off is unchanged: none of this runs without far terrain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Far tiles mesh every gap in their columns: caves and aquifers in visited and
refined chunks, and the gaps kelp and seagrass leave in water. Most are closed
on every side, so their floors, ceilings and walls cost vertices and never
show.
Before meshing, a tile fills each gap below a column's top that no
neighbouring column's open air looks into (every neighbour's top is at or
above the gap's top) with the material below it, or with the fluid above it
(kelp in water). Cave mouths stay open; unknown neighbours and cells meeting
full-detail or finer terrain count as open.
Seed 1234, RD12, LOD 64, overview: 4.93 M to 3.17 M far-terrain quads.
Follow-up to Online5277's terrain LODs (#771).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Full-detail sections leaving the render distance stopped being drawn at once,
while the distant tiles still left their columns out until the next mesh job
finished: for a few seconds after moving (longer on slow machines) a hole
opened there, lined with the tiles' seal walls. Those took the light recorded
for buried blocks, none, so the hole showed as a dark band.
Lod::take_cutout hands the renderer the columns the finished meshes leave
out. Their resident sections stay drawn, even past the render distance, and
sections unloaded while still left out are released only once the tiles
cover them. With LODs off the cutout is empty and nothing changes.
Side faces above the top of the column they face open onto air. Reduced
columns and the seals expose sides that were buried, so those parts take at
least the sky light over that top (black cliffs in far tiles). Faces into a
cave keep their recorded light.
Follow-up to Online5277's terrain LODs (#771).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and CI parity passed 322/323; the one failure, dedicated_server (two extra block updates at respawn, a server-only path this GPU change can't reach), passed 2/2 locally on main + this PR and 2/2 on main. Vendors wgpu-hal 30.0.1 with the upstream fix for gfx-rs/wgpu#9845; should fix#746 on HD 520/530, to be confirmed by the reporters on the release.
🤖 Agent: merged after verify passed (benchmark data and docs only). Four-way equal-work benchmark: Horizon uses 3.8–4.8× less RAM and starts in ~8 s; Paper's average tick is shorter at every count, which is now perf work.
Its formations:simple becomes the game's horizon:formations (its pool and
placement), and its single pool element vanilla's with its ground_level
under horizon:ground_level. Its pieced structure (one) stays out; its
processors are still dropped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The structure context gets ChunkGenerator.getBaseColumn's blocks
(StartContext::noise_column: air, water, lava, solid or other, from the
noise's minimum Y, air past its ends), which Formations' placements test.
horizon:formations is Formations' SimpleStructure: one element of its pool
at a random spot of the chunk and rotation, at the height its placement
finds under the element's corners and centre (surface, ceiling, on water
or lava, in caves on the floor, under the ceiling, buried, on lava), as
the mod draws and checks them. Single pool elements take a ground level
(`horizon:ground_level`, 1 by default as vanilla's), which moves the piece
and its terrain adaptation as Formations' does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. The site's nav links Host a server; README gains a hosting paragraph; tools/server.py and docs/server-quickstart.txt retired.
🤖 Agent: merged after verify and CI parity passed. horizon-server update (SHA-256 check, world backup, switch, health wait, automatic rollback) and rollback; 6-hourly update notice in log, status and panel. Proven end to end on the Mac mini and the ThinkPad (healthy update, crash rollback, timeout rollback, manual rollback, real GitHub download); 37 unit tests.
Better Ocean Monuments' ten processors (no fields; their behaviour is in
its Java) become vanilla's rule, the game's horizon:pick (one new
nextFloat per chance), horizon:flood_air, horizon:waterlog,
horizon:keep_world_block and horizon:yung_pillar; Better Witch Huts' log
and fence legs become horizon:yung_pillar. Its monuments are no longer
dry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' and Moog's pillar_processor, Repurposed's
bottom_pillar_processor and close_off_fluid_sources_processor, YUNG's
pillar_processor (Better Fortresses, Better Jungle Temples) and Better
Fortresses' stair_pillar_processor become the game's horizon:pillar,
horizon:bottom_pillar, horizon:close_off_fluid_sources,
horizon:yung_pillar and horizon:stair_pillar. A pillar's own processor
list is written in, with the Java processors in it dropped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Towns and Towers ships its 60 structures under kaisyn and towns_and_towers,
not its mod id t_and_t, so carry left them outside. A namespace is now the
mod's when its own files (its id's, its tag additions, the namespaces
already claimed) name ids in it and no mod of the pack has it as its id.
Compat files for other mods and pack mods' configs stay outside; of ATM10's
42 jars only T&T claims any. A replacing tag of its own is an addition.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the game's side of Better Ocean Monuments' Java processors:
- horizon:flood_air: the template's air below sea level becomes water
(its AirProcessor; the monuments' templates are mostly air, so they
stood dry);
- horizon:waterlog: below sea level, blocks that can be waterlogged are,
and water and lava on the edge of the chunk being generated are marked
for postprocessing (its WaterlogProcessor);
- horizon:keep_world_block: a placeholder takes the world's block (its
StructureVoidProcessor);
- horizon:pick gains `redraw` (a new nextFloat for each chance, as its
random processors draw) and `keep_properties` (Block.withPropertiesOf,
for its copper stairs and slabs);
- horizon:yung_pillar gains `keep_properties`, for Better Witch Huts'
fence legs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content mod's manifest may list namespaces its files use besides its id
(Towns and Towers ships kaisyn and towns_and_towers under mod id t_and_t).
They are the mod's as its id is: files load, loaders read them. minecraft,
c and horizon cannot be listed. Two mods that are on owning one namespace
is an error naming both; the game keeps the first and leaves the other's
content out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The loot loader read data/minecraft/ only, so every chest in a carried
mod's structure that names its mod's loot table opened empty. It now reads
every namespace (a vanilla root has only minecraft). Integers written as
floats (rolls 3.0, as Towns and Towers writes them) read as vanilla's int
codecs read them. 534 of the 675 non-block tables in ATM10's carried mods
now fill a chest; vanilla's chest fills are byte-identical.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the game's side of the mods' Java processors that write blocks around
a structure while its template is processed:
- horizon:pillar and horizon:bottom_pillar: Repurposed Structures' and
Moog's PillarProcessor and BottomPillarProcessor, a column from a trigger
block (or the bottom layer) down to the ground, with their nested lists
and Moog's height randomizer;
- horizon:close_off_fluid_sources: their fluid fix, which walls off source
blocks next to the structure's open blocks;
- horizon:yung_pillar and horizon:stair_pillar: YUNG's PillarProcessor
(Better Fortresses, Better Jungle Temples) and Better Fortresses' stair
supports.
They write through PlacementContext::set_chunk_block, the
ProtoChunk.setBlockState the mods call (heightmaps kept), and only inside
the chunk being generated, as the mods check WorldGenRegion's centre.
Processor lists nested in pillars are capped at 16 levels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, and a combined check with rdbt's #796 on current main (fmt, clippy, 359 tests, 14 world-gen harnesses). YUNG/Moog terrain adaptation around structures (Better Fortresses netherrack inside bridges 39–41 % → 2.5–7 %), its carry, and YUNG modifiers.
🤖 Agent: merged after verify and CI parity passed, and a combined check with rdbt's #796 on current main (fmt, clippy, 359 tests, 14 world-gen harnesses). YUNG/Moog terrain adaptation around structures (Better Fortresses netherrack inside bridges 39–41 % → 2.5–7 %), its carry, and YUNG modifiers.
🤖 Agent: merged after verify and CI parity passed, and a combined check with rdbt's #796 on current main (fmt, clippy, 359 tests, 14 world-gen harnesses). YUNG/Moog terrain adaptation around structures (Better Fortresses netherrack inside bridges 39–41 % → 2.5–7 %), its carry, and YUNG modifiers.
🤖 Agent: merged, thanks rdbt! Bulk density arithmetic, the shared Beardifier kernel and Perlin column reuse: 12.685 → 10.069 ms/chunk Overworld terrain (−20.6 %, one worker). We merged current main in (density/tests.rs: kept both test sets) and checked it together with #955/#956/#959: fmt, clippy -D warnings, 359 core/world/port-pack tests, and 14 world-gen harnesses (density, noise, structure_density, server_world_gen, world_spawn, decorated_region, …) all pass.
🤖 Agent: merged after verify passed. The renderer logs each start-up step once per device (driver, requested features and limits, surface, pipelines, first acquire/submit/present), so a native crash's last log line names the step.
🤖 Agent: merged, thanks Online5277! Terrain LOD lands off by default. With LODs off it matches main (0 differing pixels in 6 RD16 views, same FPS/GPU ms on the HD 620, same time to full render distance); CI verify and parity pass. We merged main in three times and adapted it to #905/#939 and the packed texture coordinates. The follow-ups from the review (edge band, integrated-GPU cost, lighter far meshes) we're taking on ourselves.
A native crash in a graphics driver leaves no crash report, only the game's
last lines. When the game ends with no report after its Vulkan adapter line and
before its first frame (Windows only), set graphicsApi:dx12 in its graphics
settings (the game home's and the run's own, only where it was auto) and tell
the player to press Play again, instead of the crash notice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Windows client builds wgpu's dx12 backend. With graphicsApi:auto (the
default), the renderer opens Vulkan and falls back to DirectX 12 when Vulkan
has no adapter (no Vulkan driver) or its device request fails; vulkan and dx12
pick one (horizon-graphics.txt, Video Settings > Graphics API on Windows, or
HORIZON_GRAPHICS_API; WGPU_BACKEND still overrides). Other systems keep their
one backend.
DirectX 12 keeps wgpu's indirect-draw validation: it is what passes each
draw's first instance to shaders there. Occlusion culling stays Vulkan-only and
flat draws Metal-only, as before; multi-draw and the GPU timer use DirectX 12's
own. gpu-allocator now resolves to the windows crate wgpu-hal's dx12 code
uses (0.62): with cpal's 0.54 it did not compile. A test translates every WGSL
shader to HLSL as the dx12 backend does for FXC.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A native crash in the graphics driver leaves no panic or crash report: the
last log line is all a player can send. After the adapter line, log the
driver, the features and limits the device is requested with, the surface
configuration, the pipelines (with multi-draw, occlusion culling and the GPU
timer on or off) and the first frame's acquire, submit and present, once per
device.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu 30 creates a Vulkan descriptor pool with no pool sizes while it creates
the device. Vulkan allows that only since 1.3.215; Intel's Windows driver for
6th-10th gen integrated GPUs (igvk64.dll, HD 520/530, UHD 620/630) reads
past the empty array and crashes with 0xc0000005 inside request_device, right
after the game logs its renderer (gfx-rs/wgpu#9845). Apply the upstream fix
(gfx-rs/wgpu#10124: one sampler entry when the pool would be empty), which no
wgpu release has yet, through [patch.crates-io].
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crates.io sources of wgpu-hal 30.0.1, minus its examples, lock file and
VCS metadata, so the next commit can apply one upstream fix that has no
release yet. Excluded from the workspace: it keeps its own lints.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed and local proof (sculk harness). Sculk drops every unloaded chunk's blocks in one pass: worst tick 583–609 → 38–44 ms with 40 bots.
🤖 Agent: merged after verify passed and local proof (heightmap, chunk_stage, lan, …). Chunk-packet heightmaps are built on the warm-up threads; bytes identical; lan_chunks −11–13 %.
🤖 Agent: merged after verify passed and local proof on the integration commit (8 harnesses, lan_e2e both modes). The server thread runs ahead of the workers on macOS and Windows, like vanilla's Thread.setPriority(8): 40 bots tick p99 101.5/77.7 → 32.8/32.7 ms, ticks over 50 ms 166/146 → 1/2.
🤖 Agent: merged after verify passed. The host clears the hazards box, so a dead guest no longer leaves it behind for pvp/hurt (8/10 → 0/3 failures under --server-thread).
🤖 Agent: merged, thanks Online5277! Server chat now goes through the shared chat component, so the wheel and Page Up/Down scroll it. We merged main into the branch (main had moved the atlas to self.gui.atlas); clippy -D warnings, the 6 chat tests and the LAN chat check (both modes) pass, and verify passes. Fixes#810.
🤖 Agent: merged, thanks LookImEcho-Rick! It was still marked draft, but its description was complete and our checks were done, so we marked it ready. /execute in <dimension> now moves the player across dimensions like vanilla (fixes#592). With main merged in and the #947 fix (#962), command_exec, command_parse, server_dimension_players and dedicated_server pass locally, and verify passes. If you meant to add more, a follow-up PR is welcome.
🤖 Agent: merged after verify passed and parity --changed 311/311 locally. Lithostitched apply_random, condition and set_block in the game: CTOV farms grow carrots, potatoes and beetroot, paths and mossy cobblestone appear.
🤖 Agent: merged after verify passed and parity --changed 311/311 locally on the integration commit. YUNG modifiers, piece_in_range and dead-end pools (game side).
🤖 Agent: merged after verify and CI parity passed. The carry reads data files with comments as Minecraft does; six Better Fortresses pools come back (265 → 3,765 pieces over seed 1234's Nether centre).
🤖 Agent: merged after verify passed and local proof on one integration commit of modpacks-9's PRs: parity --changed 311/311, structure-start oracle 3,153/3,153. CI parity's only failure was secure_chat, which this change doesn't touch. Stronghold rings 3/128 off → 0/128 on seeds 1234, 2, 3, 42, -7.
🤖 Agent: merged after verify and CI parity passed (command_exec 764/764 three times locally, 20 affected harnesses). /summon no longer spawns other pending entities and takes their id; the harness settle waits for entities like vanilla's. Fixes#947.
tools/server.py and docs/server-quickstart.txt are gone (horizon-server replaces them);
build_release.py builds only the client starter, and the docs point to docs/hosting.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The void can kill the guest before the hazards check tidies its stone box,
and the LAN server drops a dead guest's packets, so the box stayed: the
guest suffocated in it during pvp, and the hurt check's pig with it. The
host runs the tidying fills instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecraftServer.spin raises its server thread (Thread.setPriority(8)) when
there are more than four processors. The new horizon-threads crate makes the
same request for the dedicated server's thread and the integrated server's:
user-interactive QoS on macOS, above-normal priority on Windows, nothing on
Linux (an unprivileged thread can't raise itself).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`horizon-server run --dir srv` (what init prints) started the game in srv/ with
--control-socket srv/.horizon/engine.json, which the game resolved inside srv/,
so it crashed at every start. spawn now passes absolute paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Vega 11 on Windows (Vulkan) panicked at launch in bytemuck's cast_slice
with TargetAlignmentGreaterAndInputNotAligned, starting with the release
that shipped occlusion culling. The section-test results and the GPU
timer's timestamps were cast in place from the mapped range, which isn't
guaranteed to be 4- or 8-byte aligned. Copy the words out instead.
Fixes#953
Co-Authored-By: Claude Opus 5.5 <[email protected]>
apply_random becomes horizon:apply_random: its weighted lists, or its set
of lists at weight 1, with the inline lists carried like any other (their
Java processors dropped and counted). condition becomes horizon:condition
when the game runs every condition in it (true, all_of, any_of, not,
random_chance, matching_blocks with plain property values), with its then
and else lists carried. set_block with a simple or weighted state provider
and no block entity change becomes horizon:set_block. Anything else stays
Java and is dropped as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon:apply_random runs one processor list drawn by weight
(WeightedList.getRandom); horizon:condition runs `then` or `else` by a
test of the block (true, all_of, any_of, not, random_chance and
matching_blocks on the placed or the existing block). Both take their
random from Lithostitched's RandomSettings: seeded with the world seed
plus the name's hash, forked positionally at the block, the piece or the
structure's pivot. process_block now gets the piece's position for it.
horizon:set_block is Lithostitched's set_block with a simple or weighted
state provider: the block becomes the state (drawn at the chosen position
from the unsalted seed), keeping the properties it shares with the old one
unless preserve_state is off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged, thanks rdbt! Terrain sampler scratch buffers are reused between chunks; byte-identical output across the 22 world-gen parity harnesses on the combined commit.
🤖 Agent: merged, thanks rdbt! Spectator scroll-wheel flight speed now follows vanilla (6,791 oracle cases); verified on the combined commit with local_input and the client tests. Fixes#945.
lithostitched:reference runs the processors of other lists in turn.
Carry now writes their processors in its place, in order and references
in them too, from the mod's own processor lists. A reference to a tag or
to a list the mod lacks stays, and is dropped and counted as before.
ChoiceTheorem's Overhauled Village builds most of its lists this way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lod::update sorted every unsampled plan node each frame to take at most
four (none while every sample slot is busy), and woke the workers every
frame. It now skips the selection while the slots are full, keeps only
the best 2 * (SAMPLE_JOBS + pending) entries before sorting (a key is
listed at most twice, so the same tiles are taken in the same order),
takes the best fallback with min(), and wakes the workers only when a job
was queued. A finished chunk's detail now goes straight to the tile at
each level 0-4 holding it instead of scanning every ready tile (the others
never overlapped it).
Mac mini, RD12, LOD 256 while far terrain generates, `sample` of the main
thread over 30 s, alternating: Lod::update 10.4 % and 12.3 % of the
samples before, 3.4 % and 3.5 % after; update_world 3497 and 3373
samples before, 668 and 864 after.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
YUNG's API and MoogsStructureLib add their own kernel to the Beardifier:
a size and falloff of its own, carving or burying above and below a piece's
floor, around rigid pieces and junctions. Better Strongholds buries its
stronghold, Better Fortresses carves the netherrack over its bridges and
Moog's mega fortress carves a band around its corridors. A structure or a
pool element holding `horizon:enhanced_terrain_adaptation` now gets that
kernel (horizon_core's TerrainKernel), summed after vanilla's density as
the libraries' mixins do; YUNG's also grows the start's box by its radius.
Vanilla data never has the key, so vanilla worlds are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Better Nether Fortresses relies on three parts of YUNG's API the game did
not have:
- piece_in_range: another piece (of some templates, or any) inside the
piece's box grown sideways, below and above.
- Modifiers, applied once the layout is done, in piece order and with the
assembler's random: a condition, then actions on the piece itself.
transform swaps its template for one picked by setFeatureSeed(x, y, x)
and moves it by an offset turned with the piece; delay_generation moves
it to the end of the piece list.
- Dead-end pools: a piece that got no child though it has other jigsaws
is taken out (its parent's junction, its room and its count given back)
and a piece of its dead-end pool is chosen for the same parent jigsaw.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minecraft reads data JSON with Gson's lenient reader, which skips `//`,
`#` and `/* */` comments. Six of YUNG's template pools have them (Better
Fortresses' bridges, halls and hall ends; Better Jungle Temples' floors),
so carry refused them as unreadable and the structures lost those pieces:
seed 1234's seven Better Fortresses had 265 pieces in all, now 3,765, and
five Better Jungle Temples 170, now 196. A file that parses once its
comments are gone is carried as plain JSON.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkGeneratorStructureState's ring searches call findBiomeHorizontal,
which samples its 57x57 quarts in order through one resolver; vanilla's
climate index starts each lookup from the previous result and keeps it on
a tie. Horizon built a fresh resolver for every quart, so ties went the
other way: three of seed 1234's 128 strongholds were up to 6 chunks off
(the first one 96 blocks from vanilla's). StartContext::noise_biomes
samples a batch through one resolver and the search hands it the square.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
YUNG's single elements keep their dead-end pool and their modifiers under
horizon:yungsapi, which the game's assembler now reads: the modifier's
condition, and its transform and delay_generation actions on the piece
itself. piece_in_range carries as is. A modifier aimed at another target
than self, or another action, is counted lost.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
YUNG's API's `enhanced_terrain_adaptation` (its presets spelled out, NONE
as size 0) and MoogsStructureLib's (with its band) were counted lost. Carry
now writes them as `horizon:enhanced_terrain_adaptation` on the structure
and on the pool elements that have their own: YUNG's with every piece's
junctions and the start's box grown by the radius, Moog's with only the
terrain-matching pieces' junctions, as each library does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed. tick_blocks finds the motion-blocking height from the top of the terrain: 1.98 → 1.36 ms per tick with 10 bots on the Mac mini (−31%).
🤖 Agent: merged after verify and CI parity passed. Non-living entities (TNT, falling blocks, boats, frames…) now tick only in entity-ticking chunks, as in vanilla.
🤖 Agent: merged after verify and CI parity passed; 301/301 harnesses locally on the combination with #951/#952. Dedicated servers now spawn mobs around every player (0 → ~70 per bot, 139–144 for two, matching vanilla's caps).
Coming back to a level, the entities saved with its chunks wait in
World::loaded_entities until the next tick spawns them, as vanilla's
PersistentEntitySectionManager.processPendingLoads does. /summon pushed its
compound onto that queue and drained all of it, so the first waiting
entity took the id counted as the summon's root: after a Nether round trip
`/summon minecraft:armor_stand ...` reported "Summoned new Pig". Summon now
spawns only its own stack and leaves the queue to the tick.
The command harness's settle now waits for the entities of the chunks
around the player as well, like the oracle's settle
(areEntitiesActuallyLoadedAndTicking), and a `portal` op sends the player
through the nether portal it stands in (Entity.handlePortal:
NetherPortalBlock.getPortalDestination, teleportToPortalDestination). The
corpus's new `levels` group makes the round trip and checks the Overworld's
pig and a summon's feedback on the way back.
Fixes#947
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The soft 4 ms yield per frame (also with Unlimited budgets) made a saved
RD32 world reach full render distance about 1 s later on the Mac mini
(streaming time after launch, 4 alternating pairs: main 1.9 s mean, yield
2.8 s; t100 from a first batch of 3 pairs: 6.1 s vs 7.1 s) without better
loading frame times (max 133-210 ms vs 31-150 ms). Without a budget every
finished mesh uploads at once again, as on main. The budgeted path keeps
this branch's fixes: only loaded chunks' sections wait, and sizes count the
24-byte terrain vertex.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thanks rdbt! Verify passes. A per-worker memo of the carver biome lookups in front of the shared mutex: the resolved biome per origin is a pure function of seed, dimension and position, so results can't change, and the test checks eviction leaves the terrain identical. Paired benchmarks: +3.3–3.9 % terrain throughput at six workers (16 pairs, two sites), up to +5.9 % at size 32, no single-worker regression.
getHeightmapPos(MOTION_BLOCKING) walked every column from y 319 down,
one block lookup per y. Each precipitation roll (3 in 48 per ticking
chunk per tick) and each isRainingAt paid for it: with the chunks of 10
players ticking it was most of tick_blocks. Only air lies above the
WORLD_SURFACE heightmap the chunk already keeps, and air neither blocks
motion nor holds a fluid, so the scan starts there. LevelAccess gains
world_surface (the build limit by default, so other levels scan as before).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.tick ticks an entity only where the chunk tickets make it
entity ticking (inEntityTickingRange, and EntityTickList holds only the
entities of entity-ticking chunks). Mobs, items and minecarts already
checked it (#931); these six entity kinds ticked wherever they were
loaded, so TNT went off and sand fell past the simulation distance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Natural spawning returned without a local player, so a dedicated or
--server-thread server spawned no mobs and a LAN host spawned them only
around itself. ServerChunkCache.tickChunks now takes its spawning chunks
from every player in the level (ChunkMap.collectSpawningChunks), the
global caps from the natural spawn chunk tracker's size, and the local
caps from each player (LocalMobCapCalculator). Inhabited time and
thunder follow the same chunks (tickSpawningChunk), the wandering trader
picks a random living player (getRandomPlayer), and mobs despawn by
their distance to the nearest non-spectator of any kind.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's #880 added Source::level and Source::elsewhere for server players
teleported into another level; this branch adds Source::dimension and
moves the local player. Kept dimension (it covers what elsewhere was read
for) and level; /tp tries main's teleport_across for a server's players
first, then this branch's local-player level switch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. Ports removeWhenFarAway = false for Villager, WanderingTrader and AbstractGolem (iron/snow/copper golems, shulkers). Seed 1234 plains village: main 0 villagers, this PR 14, vanilla 26.3 server 14. 95/96 affected harnesses pass; world_spawn timed out on a busy machine, then 59/60 seeds alone (the one off is the known chunk-order flake #308). Fixes#917.
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887#931#912#916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887#931#912#916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887#931#912#916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887#931#912#916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
Sections arrive back to front (visible is sorted by distance, then key), so
the merged list only needs sorting when a distant tile adds translucent
draws, and ties now break the same way as main's reversed visible order:
with LODs off the draw order and cost are exactly main's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
processors.rs tests: keep both #926's trial spawner and vault tests and
#920's Repurposed rule-test test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps #905's single command buffer and #939's occlusion culling: distant
tiles draw in phase 1, so phase 2 stays exactly the deferred sections the
culling test reads back, and the flat path's origins include the tiles in
main's relocated upload block.
Adapts to main's APIs: block sprites and models now live in RenderAssets
(WorkerContext::render), BlockFacts behind Resources::facts(), and
terrain vertices hold the UV as 16-bit fixed point (#df58a474). Merged
distant faces repeat their sprite once per block, which the [0, 2) section
UVs cannot hold, so tile vertices now store UVs from the face's corner in
1/32-block steps (TerrainVertex::tiled, faces split at 1024 blocks) and the
instance's origin.w carries the UV step: 1/32768 for sections (decodes
exactly as before), 1/32 for tiles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. HD 620 (Vulkan/Mesa) culling off → on: RD16 +22–42 % FPS (peak unchanged, culling paused), RD32 +14–109 %; 1 % lows and CPU ms equal or better except RD32 peak's 1 % low (38.8 → 37.4). Screenshots: static views within main-vs-main noise, 21 frames after 30–40° camera jumps identical (no popping). parity section_mesh, translucency, light, hud locally; CI parity covers the rest. On only on Vulkan devices with SHADER_EARLY_DEPTH_TEST, INDIRECT_FIRST_INSTANCE and fragment-writable storage; Metal draws as before. Not yet checked on NVIDIA/AMD or Windows Vulkan: HORIZON_GPU_CULL=0 turns it off.
Sections that terrain in front hides entirely are drawn with an instance
count of 0. Phase 1 draws the sections the last test read back saw; in the
same pass every other section's box (and an eighth of phase 1's) is drawn
against phase 1's depth, front faces only, with a fragment shader forced
past an early depth test that marks the section seen. Phase 2, in a second
pass, draws what the test saw. The test is exact per pixel and needs no depth
read or Hi-Z, and every solid draw stays one entry of one list, so the CPU
records as many draws as before (Mesa unrolls a multi-draw per entry on
Intel Gen9). Culling pauses while the last result hid under a tenth of the
solid indices, with one frame in 64 testing everything.
HD 620 at RD16: +23-42% FPS in five of perf-frame-6's six views, the peak
view unchanged; RD32: +14-109%. Pixels unchanged, also on camera jumps.
Vulkan only: naga's MSL has no early fragment tests. HORIZON_GPU_CULL=0
turns it off, `all` culls every frame; `section-cull on|off` in a game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. Release dry run on this branch (run 39167763008): linux, macos, windows builds and publish all succeed; horizon-server-linux-x86_64.tar.gz (28 MB) built and import-checked (no libasound/libudev). Agent's proof: 880/879 client tests in both builds, dedicated_server/server_entities/lan on the server build, install → init → start → bot join on the ThinkPad without the libraries.
🤖 Agent: reviewed; verify passes. Equal-work server benchmark: fixed exploring speed, 300 s warm-up, chunk-work comparison per run. Horizon 1.8/4.7/11.7/23.8 ms per tick at 1/10/25/50 bots vs vanilla 11.3/34.3/50.5/stopped by its watchdog; single runs on the ThinkPad, caveats in the README.
🤖 Agent: reviewed; verify and parity pass. tools/vanilla_client.py runs on macOS (vsync off so a hidden window doesn't block the join); the owner's vanilla 26.3 client joined a Horizon server and stayed connected 4 min.
Explore bots paced at 0.8 blocks a second and a 300-second warm-up, so both servers serve the same
paths; the work table shows it. Horizon ran as horizon-engine (the server build, from its archive).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thanks rdbt! Verify passes. Checked locally merged with current main: fmt and clippy clean, cargo test -p horizon-client (885 passed), 14 harnesses including your two new oracles (player_sync, interaction_input, local_input, sprint_input, item_use, item_use_start, item_use_lifecycle, use_item_on, teleport_consume, lan, dedicated_server, server_player_combat, server_player_targets, server_entities), and lan_e2e pvp, hurt, eat, eating, clicks, chat, respawn in both modes: all pass. Fixes#903.
Village villagers are placed from the jigsaw templates without
PersistenceRequired (the village's iron golem has it), so Mob.checkDespawn
discarded every villager of a village generated more than 128 blocks from
the player, and the rest after a while beyond 32 blocks. Vanilla's
Villager, WanderingTrader and AbstractGolem (iron, snow and copper golems,
shulkers) override removeWhenFarAway to false.
Fixes#917.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. parity --changed 95/95 before the rebase; the 9 block-tick harnesses (dedicated_server byte-identical, 591 lines) and lan_e2e plates/growth/time in both modes after it; growth fails on main. Fixes#890: blocks tick from vanilla's ticket levels around every player. MSPT with 10 bots rises from ~1.6 to ~14 ms because blocks now tick at all on a server (vanilla: 40).
ChoiceTheorem's Overhauled Village swaps blocks in its templates through
Lithostitched's block_swap processor (137 lists): a map of block to
block, the new one taking Block.withPropertiesOf the old state, and the
block left alone when the new one is not registered. Carry dropped it.
- Game: horizon:block_swap, the same map; the new block's default state
with each property of the old one it has too (by name, when the value
is one of its own).
- Carry: lithostitched:block_swap becomes it.
ATM10: processors carried 136 -> 273 of 1,427 (dropped 1,291 -> 1,154).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Better Dungeons' small dungeon templates hold several chests; its Java
chest processor keeps the first, then each other one on nextFloat <= 0.2
until two have stayed, and turns the rest into cave air. Carry dropped it,
so every chest stayed.
- Game: horizon:count_cap. Of `blocks`, the first `min` a placement meets
stay, then each stays on settings.getRandom(pos).nextFloat() <= chance
until `max` have; the others become `replacement`. Counted per
placement (PlaceSettings, reset in process_infos), like the mod's
DungeonContext, set up per StructureTemplate.placeInWorld.
- Carry: betterdungeons:small_dungeon_chest_processor becomes it with the
mod's config defaults (1, 2, 0.2; chest and trapped chest).
Seed 1234, five small dungeons decorated: 12 chests -> 6.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Better Desert Temples builds its temples with placeholder blocks (orange,
red and white glass, blue and yellow concrete, red wool, polished diorite,
blank pots) that its Java processors turn into suspicious sand, sand and
sandstone, upside-down stairs, cobwebs, terracotta, TNT and pots with
sherds. Carry dropped them, so the placeholders stayed in the world.
- Game: a horizon:pick processor. A block of `block` takes the first
pick whose `below` is past one settings.getRandom(pos).nextFloat(),
else `default`; an outcome is a state or one of several by nextInt
(Util.getRandom), a new block entity or the block's own, and NBT lists
of picked strings (one float each).
- Carry: the fixed swaps become vanilla's rule; the random ones
horizon:pick with the mod's bounds (BlockStateRandomizer's sums as
floats). The processors that read or change the world around (pillars,
Diorite's waterlogging) stay dropped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without a local player (dedicated server, --server-thread) no block ticked:
tick_blocks returned early, so crops, fluids, redstone, pistons and pressure
plates stood still, and a LAN host ticked only around itself.
ticket_levels ports DistanceManager's levels: PLAYER_LOADING at 31 within the
view distance and PLAYER_SIMULATION at 31 - simulation distance for every
player in the level (the local player, LAN guests, a server's players), and
FORCED at 31. Random ticks run in simulation entity-ticking chunks that are
ticking chunks, scheduled ticks, block events and block entities in block
ticking ones, entities in entity ticking ones; the sculk blocks follow the
same rule. Saved ticks unpack when a chunk starts ticking
(startTickingChunk), not when it loads.
The hub keeps the tick-rate state current while nobody is in the game, so a
player joining a frozen server is told once (updateJoiningPlayer).
dedicated_server parity froze nothing while the client was sent its chunks:
saved fluid ticks there (a lava fall that never settles) ran before or after
the chunk was sent depending on each server's loading speed, and it failed on
main too. It now freezes the game until the client has its chunks.
lan_e2e gets a growth check: water flows and wheat grows next to a guest
1000 blocks from the host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Moog's trial spawner and vault randomizing processors keep the block and
change its block entity. The templates were saved with state that breaks
them in a new world: a vault's server_data holds
state_updating_resumes_at far in the future (it never opens) and a
spawner's spawn_data an empty entity (it spawns nothing of its own).
- Game: a horizon:edit block entity modifier takes `remove`'s keys out,
then merges `data` in. append_static now merges as CompoundTag.merge
does (compounds in both are merged), not a shallow replace.
- Carry: each processor becomes vanilla's rule processor, one rule per
block state (12 trial spawner, 32 vault), with horizon:edit: the
spawner's normal and ominous configs by id, the vault's loot table and
key (the ominous ones on ominous vaults). trial_spawner/ files get their
mobs' 1.21.1 ArmorItems/HandItems as 26.3's equipment.
ATM10: 6 more processors carried (125 of 1,427).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tool picks this machine's natives (macOS arm64 or Linux), adds -XstartOnFirstThread on macOS
and turns vsync off there: with it on, an unseen window (a locked session) blocks in
SDL_GL_SwapWindow before the client joins. docs/server.md records the join.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' jungle fortress and End stronghold still write
vertical_distance_from_start_piece, but no codec of the mod (or of Moog's
Structure Lib) reads it any more: its own structures ignore it. Carry
drops it without counting it in rules_lost, which now lists only rules
the mods do run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
betterdungeons:small_nether_dungeon is YUNG's jigsaw structure (the same
fields and YungJigsawManager assembly) behind the mod's
smallNetherDungeons config switch, which ships on. Carry maps it as it
maps yungsapi:yung_jigsaw: minecraft:jigsaw with horizon:yungsapi.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' 10 overworld mineshafts (birch, desert, icy,
jungle, ocean...) are its generic jigsaw with one change in
MineshaftStructure.postLayoutAdjustments: the layout is not centred on
the chunk; its top goes 15 blocks under the lowest ocean floor at the
chunk's middle and 16 blocks to each side (taking one off after each
side, as the mod does), never up, within y_allowance.
- Game: "layout": "mineshaft" in horizon:piece_limited runs that step.
- Carry: repurposed_structures:generic_mineshaft becomes minecraft:jigsaw
with the piece-limited layout and that key, as its generic jigsaw does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A merged or closed pull request's queued runs held the self-hosted runners
until someone cancelled them by hand. pull_request_target lets the token
cancel runs of fork pull requests too; the job checks out nothing. It
matches the branch and the head commit, so a fork's branch named like
another doesn't cancel that one's runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two things kept Repurposed Structures' mineshaft processor lists from
loading in 26.3, and with them the mod's 5 Nether mineshaft pools:
- Its three rule tests written in Java. Carry renames them to the game's
horizon:matter_phase, horizon:piece_origin_axis_aligned_linear_pos and
horizon:y_value (213 in 75 lists), counted in the report's
rule_tests_carried.
- 1.21.1's minecraft:chain, which 26.3 calls minecraft:iron_chain (its
datafixers rename it in worlds). Carry renames it in recipes, loot
tables and worldgen files: 112 files in 5 of ATM10's mods.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' mineshafts and Nether monument test blocks with
three rule tests of their own. Vanilla, and the game, refuse a processor
list naming a type they do not know, and with it every pool naming the
list: the 5 Nether mineshaft start pools in a ported pack never load.
The game now runs them under its own types, with the mod's fields:
- horizon:matter_phase (MatterPhaseRuleTest): solid (not air, no fluid,
canOcclude), liquid, air, and air or liquid with rails and chains,
optionally inverted. Structure processors now bind the block
properties to read canOcclude.
- horizon:piece_origin_axis_aligned_linear_pos: vanilla's axis-aligned
chance over the block's distance from its template's origin.
- horizon:y_value: the block's world height in a range.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: the hub's player state keeps both main's attributes (#857) and
#887's living_flags; remote.rs and lan.rs debug JSON keep swinging and
using; the guests' model tick keeps both the swing update and the item
in use; lan_e2e.py checks unioned. remote_player_eating reads main's
Option<Consumable> (#799): any consumable with particles, its own use
time and should_emit, as the local player does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. Mac mini RD32 standing, 4 alternating pairs: 116.1 → 163.3 FPS, CPU 7.09 → 4.98 ms/frame. Screenshots of 6 views plus HUD and inventory: no pixel differs beyond main-vs-main noise. parity --changed 95/95 locally. No early return after the copies encoder is taken, so staged section copies can't be dropped.
horizon-client's cpal output and its controllers (gilrs, the DualSense's HID output) move behind the
audio and controllers features, on by default, so the game is unchanged. Built with
--no-default-features it links neither libasound.so.2 nor libudev.so.1. release.yml packages that
build as horizon-engine in horizon-server-linux-x86_64.tar.gz (xtask package-server, checked by
check_imports.py --server); install.sh installs it on Linux (and --archive installs a local one);
horizon-server already prefers horizon-engine. The Dockerfile uses it too, and CI lints it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
🤖 Agent: reviewed; verify passes (client --help exits 0 without loading the game, so the start check only catches a program the system can't load). Linux installs now name the missing ALSA/udev libraries; services run through the installer's current link.
A dedicated server printed refused logins on its console as vanilla
does ("[Server thread/INFO]: Disconnecting <name> ..."), but wrote them
to logs/latest.log under the connection thread's own name
("[horizon-lan-conn/INFO]"): the file logger used the OS thread's name,
not the one vanilla logs the line from. The engine's lines now go to the
file under the same thread as on the console.
The console's times were UTC while logs/latest.log and vanilla's
console (Log4j's %d{HH:mm:ss}) use local time, so the two disagreed by
the time zone, as did the supervisor's own lines in
logs/supervisor.log and the web panel's console. Both now print local
time. Backup names keep their UTC time, as documented.
Found by the end-to-end install check on the Mac mini and the ThinkPad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first published benchmark for the working version (docs/server.md
section 10, item 4). It ran the v2026.10.18.15 release, as install.sh
installs it, against vanilla 26.3 with 1, 10, 25 and 50 mixed bots. The
servers ran on the ThinkPad (quiet, under nice 19) and the bots on the
Mac mini over the tailnet. Vanilla's default game rules applied on both;
Horizon's server runs the locator bar now.
Both servers kept 20 TPS throughout. Horizon used 3.7 to 5.2 times less
memory. Its average tick was shorter with 1, 10 and 25 bots and 18 %
longer with 50, while it generated about twice the new terrain a second
(the exploring bots fly as fast as each server sends chunks). Its P99
was lower at every count.
benchmarks/server-mixed-2026-10-18/ holds the README, the report and the
machine's load; docs/server.md links it from sections 9 and 10.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the server thread, lan_e2e --check light failed its border step (#891):
the guest's border chunk still had block light 0 next to glowstone placed
in the chunk past it. The server was right; the check was not.
same_light() waits until the host's light equals the guest's, but on the
server thread the host's game is a client too, and both are dark until
the light update arrives. They agreed at once, same_light returned, and
the next line read the guest's light before the update came.
The check now waits up to 100 ticks for the guest's border light, then
compares the host's with it.
With temporary logging on the server thread: the glowstone lands in a
finished chunk that isn't lit (the ring past what the guest tracks), so
the edit queues a full light job for its neighbours. The border chunk's
job then relights sections 15 to 17, and Hub::broadcast_light_changes
sends them to both clients as a tracked border chunk, as
ChunkMap.isChunkOnTrackedBorder does. Nothing in the engine changes.
Fixes#891
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. Bit-exact against the per-corner oracle (unit test over a dense grid, 7 seeds, wrapping extremes), CI parity passed, and the surface pass is about 10% faster in chunkgen_bench. Thanks rdbt!
On a Linux without ALSA's library (most minimal server images), the
game exits at once with the loader's "error while loading shared
libraries: libasound.so.2". The supervisor took that for a crash and
restarted it five times over two and a half minutes before giving up
with "see logs/latest.log", which holds nothing about it.
Now:
- `run` starts the game with --help first (it loads no game files) and,
if the system can't load it, stops at once with the loader's line and
the packages to install on Debian/Ubuntu, Fedora, Arch and NixOS;
- `init` prints the same as a warning after "Game:";
- install.sh lists the libraries `ldd` can't find, with the same advice;
- docs/server.md says so in the install section.
Also, a service installed without the installer's launcher on the PATH
ran `<dir>/releases/<tag>/horizon-server`: Linux reports the running
program with links resolved, so the unit pinned that release and an
update never reached it. The fallback now goes through `<dir>/current`
while it points at that release.
Found by the end-to-end install check on the ThinkPad (NixOS, no
nix-ld libraries) with v2026.10.18.15.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. Thanks rdbt for the consolidation and for merging main yourself! On de6f15e6: fmt, workspace clippy, horizon-client tests and the client build pass, and 19/19 covering harnesses pass (options, world_reopen, lan, dedicated_server, block_removal, section_mesh, resource_pack, menu_click, screen_layout, the pack, world-options, chat and container screens, and the consumable ones). We'll handle the two follow-ups noted in the comment above (the 120 s fatal timeout, and the backdrop terrain built on the event loop).
init and run looked for asset-index.json to recognise a prepared folder,
but horizon-prepare writes that file only with the optional sounds
(`horizon-prepare sounds`). A server prepared the usual way, without
sounds, was never found: init said "no prepared game files" and listed
the folder that held them. They are now recognised by
horizon-resources.json, the manifest every preparation writes and the
one the game itself reads.
Found by installing v2026.10.18.15 with install.sh on the Mac mini and
preparing from the release folder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. Matches 26.3's KeyboardHandler.keyPress: the F3 toggle isn't conditional on a world or the absence of a screen, and the title counts as an open screen so it gets no gameplay keys. Fixes#811. Thanks rdbt!
wgpu-core ends its Metal command buffer at every pass: each render or
compute pass costs two command buffers (the pass and its barriers), each
run of copies between passes one more, and Metal creates and commits every
one on its own. The frame made about 18: the section copies in their own
encoder, the queue's pending writes for the HUD's fire animation, copies
before and after the flatten pass, and the screen effects' and HUD's
vertex uploads between later passes. At render distance 32 that cost 2-5 ms
of submit per frame, mostly waiting on the command queue's lock.
The frame now records after the section copies in their encoder, stages
the fire animation and the HUD's glyph patches through the frame's belt
instead of Queue::write_texture, and records every upload (screen
effects, HUD, flatten inputs, indirect draws) before the first pass.
The commands and their order within each pass are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A `fly` benchmark replied with frame times only. With single player on its
server thread, the world's ticks no longer show in the frames, so a flight
could not tell how long the server took.
The summary now has tick_ms and tick_ms_max: the world's smoothed tick time
(the F3 server line) averaged and at its highest over the flight. On the
server thread it is the server's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Block triggers counted only the host's own player and the minecarts, so a
guest stepping on a plate did nothing. The guests in the level, not
spectators, now trigger them through Entity.checkInsideBlocks and are
counted by the plates' entity queries. On the server thread no block
ticks run at all yet (#890).
lan_e2e.py plates: a stone plate under the guest, on both sides.
Part of #848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. Fixes#861: a ctl tp/fly move is sent even while the destination chunk isn't loaded, the single-player owner is exempt from the move check as in vanilla, and wait-ready waits for the level around the player. lan, dedicated_server, server_entities and server_dimension_players pass.
🤖 Agent: Merged. CI parity and verify passed, and on current main with this branch, horizon-client clippy is clean and all its tests pass (873 lib + integration). Consumables now finish as in vanilla 26.3.
With the world on its server thread, a horizon-ctl `tp` or `fly` moved the
window's player to chunks the server had not sent. The client skips its
tick while its own chunk isn't loaded, so it never sent the move, and the
server kept streaming the old area: "loading terrain..." forever.
- The client sends a move made where nothing is loaded (by horizon-ctl;
a player who walks there has already sent it), so the server streams
the chunks there.
- The integrated server doesn't check its owner's moves, as vanilla's
`shouldCheckPlayerMovement` (`isSingleplayerOwner`); a long `tp` was
"moved too quickly".
- `wait-ready` on a server also waits for the level around the player
(`PlayerLoaded`); the status shows it as `server.loaded`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BrewingStandBlock opens BrewingStandMenu for a guest: the host clicks it on
the menu engine over the stand's slots (one bottle per slot, ingredient and
fuel slots, quick moves by item) and sends its four data slots
(BrewingStandBlockEntity.dataAccess). The guest opens minecraft:brewing_stand
menus, maps their slots, predicts clicks on Kind::Brewing and shows the
times. Reported by Tydragon00.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Neither side sent LivingEntity.DATA_LIVING_ENTITY_FLAGS for players, so
no one saw another player eat, draw a bow or raise a shield. The host now
sends the item in use and its hand for itself and every guest (always:
the trackers send only the entries that changed, and stopping is a change
back to 0), and draws its guests' bodies using their items. A guest's
client starts and stops the other players' item use from the flags and
drops their eating crumbs at their mouth (Consumable.onConsumeTick); the
eating sounds already come from the server.
lan_e2e.py eating: the host and the guest each see the other eat bread,
and stop.
Part of #803
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host ignored ServerboundPunchPacket, so a guest mining or hitting
swung only on its own screen; only the first swing a guest's use ever
started went out, the host's own swings never did, the host drew no swing
on guests' bodies and a guest's client ignored
ClientboundSwingAnimationPacket.
The host now swings the guest's hand on a punch (handlePunch), sends each
tick's guest swings to the players around and the host's own to the
guests, and swings and ticks the guests' bodies it draws. A use the server
ran that swung goes back to the guest too: Horizon's client does not run
uses itself, so it never predicted that swing (the hoe). The guest's
client swings itself or the other entity on the packet.
lan_e2e.py swings: mining swings seen from each side, and the guest's own
hoe swing.
Part of #848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host told guests of a hurt mob with EntityEvent 2, which 26.3's
clients read as a kinetic hit, not a hurt. It now sends
ClientboundDamageEventPacket (ServerLevel.broadcastDamageEvent) with the
damage type and the attacker, and the guest's client restarts the hurt
flash and jolts the walk animation (LivingEntity.handleDamageEvent).
lan_e2e.py hurt: a pig the guest hits and one the host hits flash on
both sides.
Part of #848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The guest's client closed every menu it had no screen for, so hoppers
(MenuType.HOPPER) and dispensers and droppers (GENERIC_3x3) did nothing.
They open as their 5 and 9 slot screens now, drawn by the menu's type
and under the server's title.
lan_e2e.py machines: each opens with its slots and keeps a stack the guest
shift-clicks in.
Part of #848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host sent the overworld clock's rate even while /time pause held it
(ServerClockInstance.packNetworkState sends 0 when paused), so a guest's
day kept running. A guest also moved its clock by the rounded rate each
client tick and never caught up when the server's game-time sync moved
game_time on, so a guest whose game lagged fell behind for good.
ClientClockManager.tick advances the clock by the game time passed, with
its partial tick; the guest now does the same.
lan_e2e.py time: the paused clock and a guest stalled for 4 seconds.
Part of #848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client joined to a server (and the window on the server thread) keeps the
server's chat lines in its own list. The event log only counted the local
chat's lines, so `wait-for chat` never saw a line from the server.
The remote client counts the lines it receives. The event log reads that
count and those lines while connected.
lan_e2e gets a `chat` check: a line the host sends reaches the guest's
event log, and on the server thread the host window's too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
They are Repurposed's generic jigsaw with a start check of their own:
carried as minecraft:jigsaw with the piece-limited layout and the
check named in horizon:start_checks.repurposed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' city_nether, shipwreck_nether and stronghold_end
types are its generic jigsaw with one more start check each. The
`repurposed` key of horizon:start_checks names the check:
- city_nether: no block from sea level + 20 to sea level + 45 at the
corners of the eight chunks around;
- shipwreck_nether: no solid, filled block in a 33x30x33 grid above the start;
- stronghold_end: land at least 45 above the bottom at 70 and 35 blocks
in each direction and at the start.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Carry used to take every processor written in Java out of its list (#822),
so spawners kept what the template has: an empty spawner in Moog's
templates, a placeholder pig in Better Dungeons'. The new processors module
rewrites the ones with a data form before that:
- Moog's Structure Lib's and Repurposed Structures' spawner randomizing
processors become horizon:spawner_mobs, with Repurposed's rs_spawners list
written in (weight-0 entries left out, as its loader does);
- YUNG's Better Dungeons and Better Desert Temples spawner processors (a
spawner or a placeholder block becomes a spawner of a fixed mob) become
vanilla's rule processor with an append_static block entity, the entity in
26.3's shape (equipment, drop_chances);
- Moog's vanilla loot swap is a no-op while its "replace vanilla" preset is
off, as they ship, and is taken out.
Each is counted in carry-report.json's processors_carried. The game's own
horizon: types stay in the lists.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Moog's Structure Lib and Repurposed Structures register a spawner
randomizing processor in Java: each spawner of the template gets one mob of
a weighted list, picked with settings.getRandom(pos).nextFloat(), and the
spawner NBT they write (Delay ... SpawnRange as shorts, the mob as the only
potential and the next spawn, optional custom spawn rules). With nothing to
pick the spawner becomes the replacement block.
horizon:spawner_mobs does the same from data; horizon-port-pack writes it in
place of theirs. Without it carry drops the processors, and Moog's templates
keep their spawners empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A rule processor's position predicate and block entity modifier types are
identifiers: vanilla reads "always_true" as minecraft:always_true. Horizon
matched the full string, so a list writing the short form failed, and with it
every pool naming the list. ChoiceTheorem's Overhauled Village writes
"always_true" in four village pools (plains, plains fortified, halloween), whose
houses then never generated.
The two decoders move out of Rule's decode into PosRule::decode and
Modifier::decode, which normalize the type first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With single player on its server thread, the window is a client of the
server. The agent's `command` went out as the owner's chat and returned no
output, so harnesses that read a command's feedback (lan_e2e trials) failed.
Control `give` and `time` changed only the window's copy of the world.
The server now runs them as single player's console does without a server
thread (`as_console`): as the world's owner, in the owner's level. The
feedback goes to the owner's chat and comes back to the agent. `give` runs
`give @s`, and `time` sets the server's clock too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after #855 and the real verify. block_removal 4000/4000 including 319 mining cases; dispenser, crafter, composter, mace and use_item_on pass.
🤖 Agent: Merged after the real verify passed. Prism's metadata renames native jars (same sha1), so prepare now also looks under Prism's name, still checked against Mojang's sha1, with nothing downloaded. Launcher and horizon-prepare tests pass.
With HORIZON_SPIKES=MS, a dedicated or integrated server tick longer than
MS ms is logged with the server thread's CPU time over the tick and the
tick's spans (tick_mobs, streaming, ...). A tick with much more wall time
than CPU time waited for a lock or for a core.
The spans recorded on a server thread were never taken before, so they
grew for as long as the server ran with HORIZON_SPIKES set.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every spawn attempt (`NaturalSpawner.getRandomPosWithin`) asks for the
WORLD_SURFACE height of a column. Horizon scanned the column down for it
each time, a chunk-map lookup and a block-name compare per block. Vanilla
reads the chunk's heightmap.
Chunks now keep WORLD_SURFACE: built on the first read, and dropped by a
`set` that can move a column's surface (`Heightmap.update` leaves writes
two or more below it alone). Natural spawning, the spawn rules' bat check
and beacons read it.
The unused `heights` field (never written after `Chunk::empty`) is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.removePlayerImmediately and addRespawnedPlayer: when a player
respawns, every other player tracking it gets RemoveEntities and the next
tick pairs it again as a new entity. The hub only cleared the respawned
player's own tracker, so the others kept the dead body, lying down, wherever
the player went (the host window too, on the server thread).
The in-frame LAN host's death now reaches its guests (LivingEntity.die's
EntityEvent DEATH), and its respawn goes through the same path.
lan_e2e gets a `respawn` check: the host dies and respawns (and on the
server thread the guest too), and the other side's client draws it dead,
then alive. `status` lists the other players a client draws dead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test_block_removal_parity gains a "mine" removal: VanillaDispenserOracle
calls the real ServerPlayerGameMode.destroyBlock for a survival player holding
a pickaxe, axe, shovel, sword, shears, a mace or nothing, and Horizon's
dispense fixture calls break_block, the path every player's mining takes (the
single player game, the server thread and LAN guests). It compares the drops,
the spilled contents, experience, sounds, events, statistics and the held
stack. On main it shows every mined container losing its contents (#844, fixed
by #855) and three gaps of Horizon's own, fixed here:
- ItemStack.mineBlock: an item with a tool component counts as used, and wears
its damage_per_block (2 for swords, the mace and the trident), also the mace
and trident, whose tool component has no rules. Shears wear on every block
but fire (ShearsItem.mineBlock).
- DecoratedPotBlock.playerWillDestroy: a pot mined with an item in
breaks_decorated_pots, unless an enchantment prevents it, cracks first and
shatters.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The generator's structure-start cache was one Mutex<HashMap>. Each
generated chunk reads the 289 start chunks around it for its references,
and the server thread reads the starts of the chunks its mobs and spawn
attempts are in (`structures_reaching`). While workers generated, the
server thread spent most of its start lookups waiting for that lock.
The cache is now 64 RwLock shards keyed by a hash of the chunk: a lookup
read-locks one shard. Vanilla keeps the starts on each chunk
(`StructureManager.getStartForStructure` -> `chunk.getStartForStructure`),
so its lookups share no lock either. The first starts built for a chunk
are still the ones everyone keeps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the real verify passed. Waypoints::tick −33% (166 → 111 µs median), with byte-identical packets (530,834, same hash). locator_bar, lan, dedicated_server and server_dimension_players pass.
🤖 Agent: Merged. Entities::tick −37% (112–116 → 71–75 µs median per tick at 40 bots + 570 entities), with the same 14,159,154 packets before and after. 9 covering harnesses pass (server_entities, lan, dedicated_server, server_dimension_players, locator_bar and the entity ones); main has no lan/ changes since.
🤖 Agent: Merged after the real verify passed. Combined check on this branch: fmt, clippy, workspace tests, and the worldgen, structure_density, chunk_generation and world_spawn harnesses pass. modpacks-6's own integration passed 298/298 harnesses with the start oracle at 35/35.
🤖 Agent: Merged. Mesh scheduling while flying at RD32: 0.62/0.72 → 0.36/0.22 ms per frame; at rest with ~12,000 waiting sections: 0.30–0.91 → 0.06–0.22 ms. Same checks as #865.
🤖 Agent: Merged. Terrain vertices go 28→24 B: vertex memory −14%, Metal GPU 0.62→0.54 ms. HD 620 FPS is unchanged (vertex size isn't its bottleneck). Screenshots: the only differing pixels are on water, at main-vs-main noise level, with identical drawn sections. 95/95 covering harnesses on an integration with #866 and #867.
A moving player updates its connection with every other player, three
times a tick (updateWaypoint, then updatePlayer at the position change and
at ServerChunkCache.move), so with 40 players there are about 4800
updates a tick. Each looked the connection up twice and both players up
four to six times. Now an update finds the connection's place once and
resolves the two players once, then checks, updates or remakes it there.
The same packets go out, in the same order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity tracking still asks every client about every entity each tick
(vanilla re-checks each pair whenever the player sends a move, so for
moving players it does the same), but the check is cheaper:
- each entity's position, id, range and chunk are read once a tick into
a compact list, not from its EntityState once per client;
- each client looks up each of the tick's chunks once in the chunks it
holds, not once per entity in it;
- an entity with no changes and no client starting to track it skips the
loop over the clients.
The decisions and the packets are the same, in the same order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A value back at its default is left out of an entity's state, so the
tracker never told clients that a player stopped crouching or a mob stopped
burning: Entity's shared flags and pose now go back to 0. On a client of a
server (the host window on the server thread), another player's crouch flag
now reaches their model and name tag.
From #731's server-thread list; lan_e2e's walls check passed in-frame and
failed with --server-thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the real verify passed. Output is byte-identical (palettes / region files), and 17 covering harnesses passed with both PRs on main 256f7c20 (since then main has only #751, a settings UI change).
🤖 Agent: Merged after the real verify passed. Output is byte-identical (palettes / region files), and 17 covering harnesses passed with both PRs on main 256f7c20 (since then main has only #751, a settings UI change).
Prism's metadata moves a native jar's classifier into its artifact name
(com.mojang:jtracy:1.14.38:natives-windows becomes
com.mojang:jtracy-natives-windows:1.14.38) and Prism stores libraries at their
name's path, so every native jar of a Prism 26.3 install is in
com/mojang/jtracy-natives-windows/1.14.38/jtracy-natives-windows-1.14.38.jar,
not at the version JSON's path. The bytes and sha1 are Mojang's. Preparing
from a linked Prism folder stopped on the first one (jtracy on Windows).
horizon-prepare now looks there when the version JSON's path is missing; the
copy is still checked against the version JSON's sha1. The missing-file hints
no longer tell players of other launchers to install the official one.
Reported by famescare on Discord.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundUpdateAttributesPacket, as vanilla's ServerEntity sends it: every
client-syncable instance when an entity is paired, the changed ones with the
dirty entity data, and a player's dirty set to the player itself.
- Mobs and armor stands report the instances vanilla would have created
(movement speed, finalizeSpawn's bases and modifiers, the baby modifier,
effects', equipment's and /attribute's), players their LiveAttributes.
- Guests get the per-tick movement speed instance and the creative reach
modifiers (ServerPlayer.updatePlayerAttributes).
- cube::set_size sets the attribute bases as AbstractCubeMob.setSize does,
so a summoned slime has its size's health and speed.
- server_entities compares attributes again, the player's own too, with a
slime and a scaled pig in its scene.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reads every chunk of a saved dimension, then times SaveDir's save_later and
flush against the same encoding and compression done on one thread, and
checks the written chunks byte for byte.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sections waiting to be remeshed were one hash set that the render thread
walked section by section every frame, though every check (column loaded,
in view, its 3x3 lit, its region) holds for a whole column. At RD 32 about
12,000 sections wait at the edge of the lit area at any time. They are now
kept per column (a bit per section index), so each frame looks at each
column once and meshes all of its ready sections together.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Relighting block edits and relit chunks collected every changed cell,
sorted and de-duplicated them, then marked the sections around each cell
for remeshing. Vanilla's light engine reports changed sections to
LevelRenderer instead. LightChanges now keeps, per changed section, which
of 27 border classes (low face, inside, high face per axis) its changed
cells fall in; that gives exactly the same dirty sections and light log
without keeping or sorting the cells.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TerrainVertex goes from 28 to 24 bytes: the UV becomes two u16 in 32768
steps per unit, which hold model UVs (multiples of 1/16 and their halves)
exactly and decode exactly in terrain.wgsl. The position stays a float:
fixed point moves the edges of offset plants and rotated elements by a
fraction of a pixel.
Section vertex memory drops 14% (RD 16: 380 -> 325 MB, arena 503 -> 369 MB);
pixels are unchanged on Metal and Vulkan outside animated water.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. CI parity passed 94 of 95. The one miss, tooltip_display, passes locally on main with this branch, and so does options. Thanks LookImEcho-Rick!
The writer thread encoded (NBT) and zlib-compressed every chunk of a batch
one after another before writing them. A batch of at least 8 chunks is now
shared out to scoped helper threads (at most half the hardware threads),
which take the next chunk as they finish one. The results are put back in
submission order and written exactly as before, so the region files are
byte-identical: each chunk's bytes depend only on the chunk, and sector
allocation follows the same write order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
palette_ids (behind PalettedContainer::pack, every saved and sent section,
and from_values, every generated one) scanned the palette for each of the
4096 values while it had up to 16 entries, then hashed every value with
SipHash. A run of one value is now matched once, and past 16 entries a
thread-local table indexed by value (block states and biomes are below
65536) replaces the hash map; only the palette's entries are set, and they
are cleared before returning. The output is unchanged: palette in
first-occurrence order and the same ids.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. tick_lan is 40–55% cheaper and MSPT about 20% lower (40 bots, ~600 entities). The agent ran 95/95 harnesses (--changed) with #842 on main 4b43242a. The orchestrator then ran clippy and the lan tests on current main with both: clean.
🤖 Agent: Merged. light_chunk is 28% faster, light parity shows 0 differing values against vanilla 26.3, and CI parity passed on a merge with main after #824's light changes. Thanks rdbt!
🤖 Agent: Merged. Reviewed against vanilla: landing comes from Entity.move's vertical collision, the landing position is blockPosition(), velocity is zeroed per axis, and placement goes through Level.isUnobstructed with the collision shape. Locally on this head, fmt and clippy are clean, and contextual_collision, level_place, placement, powder_snow_collision and world_movement all PASS. Thanks LookImEcho-Rick!
Their jigsaw structures, the Nether variants too, get a
horizon:piece_limited key for the game's piece-limited assembler:
y_allowance, burying_type, cannot_spawn_in_liquid, the pools whose
fallbacks ignore the bounds, the Nether variants' land search and ledge,
and each structure's piece counts from the mod's rs_pieces_spawn_counts
or msl_pieces_spawn_counts files. use_bounding_box_hack becomes vanilla's
use_expansion_hack. Lithostitched's jigsaw stays vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
YUNG's enhanced random spread becomes vanilla's random spread with its
enhanced exclusion zone as horizon:exclusion_zone; the desert and jungle
temples' placements add horizon:avoid_biomes (no ocean or river within 48
blocks of the chunk's middle at y 120); Better Strongholds' adds
horizon:ring_sections. A YUNG's structure the game can start gets
horizon:disables with the vanilla type its mod's mixin turns off; Better
Mineshafts, a Java structure type, leaves vanilla's mineshafts in place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A jigsaw structure with "horizon:piece_limited" is laid out as
PieceLimitedJigsawManager does (the same code in both mods): a fresh
random seeded by the chunk, the start piece drawn uniformly and moved to
the surface at its center, a volume 120 blocks down and 180 up, children
drawn by weight from the pools' entries in order, pieces capped per
template and some required (retried up to 40 times), pools whose
fallbacks ignore the bounds, y_allowance clipping. After the layout, the
structure's postLayoutAdjustments: the pieces centred on the chunk corner,
then buried (lowest corner, average land, lowest side) or, for the Nether
variants, moved onto the highest or lowest land or a fixed height.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged. CI parity passed 88 of 89 harnesses. The one miss, dedicated_server (a known intermittent failure), passes locally on this head. Thanks rdbt!
EntitySelector.findPlayers and EntityArgument's player targets on a
hosted world or dedicated server: every guest other than the one in place
is Target::Guest, found by name, UUID or selector, and what a command does
to them (give, gamemode, tp, kill, effect give/clear, data) runs with
their state swapped in, their own messages going to their chat. The
console, operators and command blocks can all target them; execute
as/at reads their position, rotation and eye height; a teleport into
another level (execute in, the console's Overworld, a destination in
another level) moves them there.
as_guest splits into enter_guest/leave_guest so that guest turns nest,
and a guest's commands run with the hub back in self.lan (queued per
packet), so they reach the other players (and /kick works for ops).
Item and advancement names and player names in feedback take vanilla's
component shape; /kill on a guest sends vanilla's death message.
Harness: tools/test_server_player_targets_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerWaypointManager's per-tick work for every moving player copied each
connection out of its row (its icon's style string too), scanned the row
again to put it back, copied the lists of players and waypoints it walked,
and looked players up through SipHash. Connections are now updated where
they are (the same row order and the same packets), the lists are walked
by index and the maps use FxHash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every guest had its own copy of what it was last told about every entity,
so each tick compared each entity's data and built and encoded its update
packets once per guest. A level now keeps one ServerEntity state per
entity (ChunkMap.entityMap), whose changes are worked out once a tick,
encoded once and sent to every guest tracking it; a guest's Tracker is the
set of entities it is paired with. The update cadence is the entity's own
tickCount, as vanilla's, and late clients are paired with the data the
others hold. The per-tick copy of every entity state, the id lists and the
packet lists are gone too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three keys a carried mod's data can hold, which vanilla data never has:
- "horizon:ring_sections" on a random spread (BetterStrongholdsPlacement):
starts only in the odd rings around the origin, with Java's int math.
- "horizon:avoid_biomes" (BetterDesertTemplePlacement and
BetterJungleTemplePlacement): no start when one of the biomes is found
around the chunk's middle block, as findBiomeHorizontal samples it.
- "horizon:disables" on a structure: every structure of the listed types
fails to start, as YUNG's DisableVanilla*Mixin makes
ChunkGenerator.tryGenerateStructure return false; in a set of several
structures the next one is tried, as in vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged: decoration_order::vanilla_order on flat arrays (RD32 8.14→1.06 ms per call; ticket order unchanged, checked against the old version). Combined on current main with #834/#835: clippy, client + world tests PASS.
🤖 Agent: merged: section uploads through one reused staging buffer and one encoder, as vanilla's SectionRenderDispatcher (RD32 exploring: FPS 43.5→56.0, CPU 23.7→18.6 ms/frame, p99 122→106 ms; sections drawn identical in 12 views). Combined on current main with #834/#835: clippy, client + world tests PASS.
Rust's bool parser takes only true/false, so the fixture failed on Python's
True/False; Java's Boolean.parseBoolean ignores case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A jigsaw structure with "horizon:yungsapi" (min_y/max_y) assembles the way
YUNG's API's JigsawManager does: the start piece by priority then weight
from the shuffled raw entries, children breadth-first, pool or fallback per
depth, and per-element rules from the element's "horizon:yungsapi": a name
capped by max_count, a depth range, a priority, ignore_bounds and a
condition (true, false, not, all_of, any_of, depth, altitude,
random_chance, rotation, piece_in_horizontal_direction).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla reads structure templates with NbtAccounter.unlimitedHeap()
(TemplateSource.readStructure). Horizon used the default 1,000,000-node
cap, so a template past it loaded as empty: YUNG's Better Ocean
Monuments' start piece (115x64x126 blocks) never started. Limits::templates
keeps the 256 MiB byte cap and drops the node cap, for worldgen and /place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged: LAN guests and the server-thread game keep each chunk's light and get vanilla light updates (travel CPU −17%, light_chunk −35%; lan_e2e light PASS with and without --server-thread).
🤖 Agent: merged: brewing stands tick in the level's single ticker list, in vanilla's order. perf-ticks-7's four PRs together on main d85a3603: clippy, 861 client + world tests, 19 harnesses (light, lan, dedicated_server, save_roundtrip, world_reopen, disk, datafix ×6, world, server_world_gen, block_entity ×4, chunk_stage) PASS.
🤖 Agent: merged: chunks saved in place in their region file, as vanilla (save into a full region 80.3→7.1 ms). perf-ticks-7's four PRs together on main d85a3603: clippy, 861 client + world tests, 19 harnesses (light, lan, dedicated_server, save_roundtrip, world_reopen, disk, datafix ×6, world, server_world_gen, block_entity ×4, chunk_stage) PASS.
Queue::write_buffer creates a staging buffer for every call: on Metal a
kernel call to make the buffer, then page faults while the mesh is copied
into fresh memory. While exploring at render distance 32 the render thread
made two such writes per uploaded section, plus one for its flat-draw ids.
Section meshes and flat ids now go through their own StagingBelt (4 MiB
mapped buffers, reused once the GPU has read them) into one encoder that
the frame's submission runs ahead of its own commands. A page of the
section arena that grows records its copy into that encoder too, so copies
recorded before the grow reach the grown buffer. A frame that is not drawn
still submits the copies, and more than 64 MiB of them are submitted early.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A remote client (a LAN guest, or the game on its server thread) threw away
the light in each LevelChunkWithLight and relit the column and its
neighbours itself. It now applies the sky and block layers as
ClientPacketListener.applyLightData does, and relights only block changes,
as vanilla's client does.
The hub now sends ClientboundLightUpdatePacket for the light sections that
changed in a column to the players for whom it is on the tracked border
(ChunkHolder.broadcastChanges), so light from blocks a guest doesn't track
reaches it.
horizon-ctl gains 'light X1 Y1 Z1 X2 Y2 Z2', and lan_e2e a 'light' check
comparing the guest's light with the host's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lithostitched's add_structure_set_entries and add_template_pool_elements
modifiers become horizon:add_to files under worldgen/<kind>/horizon_added/,
upgraded as the mod's own pools are. Their load conditions (lithostitched's
predicate, NeoForge's, else Fabric's) are checked against the mods ported
along; other modifier types and tag targets are counted, not carried.
ChoiceTheorem's Overhauled Village adds its villages and outposts to
vanilla's sets in Java code; carry writes them with its default config
(small, medium and large villages weighted 10, 4 and 1, outposts 1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content mod's structure set or template pool file holding
"horizon:add_to" appends its entries to the sets or pools it names
(vanilla's, another mod's) instead of being one of its own, as
lithostitched's add_structure_set_entries and add_template_pool_elements
modifiers do on NeoForge and Fabric. The content root merges them after
every mod's own files, in mod and file order; a name nobody defines is
skipped. Vanilla data never has the key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged: worldgen block states rewritten to 26.3's format (1,005 files in 17 mods). Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
🤖 Agent: merged secure chat as vanilla 26.3's server (chat sessions, signed chat and commands, enforce-secure-profile; secure_chat harness identical to vanilla offline and online; 7 server harnesses pass).
🤖 Agent: merged: the integrated server follows the window's render distance past 32 (fixes#804), and the lan_e2e hazards check now finds the guest by name (the server-thread hunger gap was a test bug).
IntegratedServer.tickServer: the server takes the window's render and
simulation distances when they change ("Changing view distance to N, from
M"), tells every player (SetChunkCacheRadius, SetSimulationDistance), and
each player's chunks follow, up to what they asked for. The integrated
server no longer clamps the view distance to 32, and a remote client sends
its render distance again when it changes (Options.broadcastOptions),
past 32 too. Fixes#804.
lan_e2e: a distance check (--server-thread), and the guest is found by
name in lan status, where the server thread lists the owner too: the
hazards check read the owner's food (the "no hunger from moving" gap).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_block_entity_anims rebuilt a map of every drawn block entity, sorted
it and swept the animation states against it every tick. The renderer now
keeps a revision that changes when its block entities do, and the sorted
ticker list is rebuilt only then. The animation states use FxHashMap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chat session updates with their profile key checked against the services'
keys, signed chat and the message chain, last-seen acknowledgements, the
signature cache, signed say/me/msg, enforce-secure-profile, and vanilla's
log lines. Offline servers and players without a chat session chat
unsigned, as on vanilla. tools/test_secure_chat_parity.py compares both
servers with test keys and a mock services server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' and Moog's jigsaws check biomes and terrain height in
a radius and refuse to start in liquid; their spreads keep a minimum
distance from the world origin and away from several other sets. Carry maps
them to vanilla's jigsaw and random spread and writes those rules under
horizon: keys; the game now runs them (start_checks.rs, the spread's
min distance and exclusion sets). Vanilla data has none of the keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game, like vanilla, refuses a processor list holding a type it does not
know, and with it every pool naming the list, so those structures did not
generate at all. Carry now takes the library types out of processor lists
(files and inline ones in pool elements) and counts them by type in the
carry report: the structures generate, undecorated. On ATM10's carried
mods: 1,427 processors of 146 types; pool decode failures 31 -> 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Repurposed Structures' and Moog's jigsaws and spreads add rules on top of
vanilla's; mapped to vanilla they were lost, and Moog's spreads structures
1.65 times as far apart as its files say, so MVS alone started 9x as often
as on NeoForge. Carry now writes the start rules under
horizon:start_checks, the minimum distance and the super exclusion zone's
sets (from the mod's structure set tags) under horizon: placement keys,
and scales Moog's spacing. Layout rules (piece clipping, burying) are still
lost and counted in the carry report's rules_lost.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1.21.1 writes a block or fluid state in worldgen JSON as {"Name", "Properties"};
26.3's codec reads {"id", "properties"} and refuses the old shape, so every
processor list, feature or pool holding one failed to decode. Carry now
rewrites them. On ATM10's carried mods: 1,005 files in 17 mods.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game and the assets step each parsed facts.json into a JSON tree, then
copied each state into an NBT tree, before building the registry. With a
pack's 214,000 states each step peaked at 816 MB for a 139 MB registry.
The registry now deserializes the rows it needs straight from the file
(each row's state and its air and fluid columns, and the defaults):
registry peak 816 -> 153 MB, assets peak 816 -> 235 MB, RSS after the
assets step 1,344 -> 442 MB. Vanilla: 138 -> 27 MB.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Saving a chunk read every chunk of its region file and wrote them all to a
new file. The writer now writes only the saved chunk, into the first free
run of sectors (RegionBitmap), points the header at it and then frees its
old sectors (RegionFile.write / clear), with one flush per region and batch.
Readers and the writer share one open file per region. A chunk queued again
before the writer reached it is written once, by its newest batch
(IOWorker.pendingWrites).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brewing stands were the last server ticker with a pass of its own over the
whole block entity map every tick. They now tick in their place in
Level.blockEntityTickers, as BrewingStandBlock.getTicker makes them: in
list order, only in block-ticking range and inside the world border, with
their bottle states written as each one ticks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged: creaking heart scan only in sections whose palette holds a heart (240→3 ms per 15 s). Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
🤖 Agent: merged: vanilla FULL ticket square per server player (RD32 finished chunks 8809→4761, spawn CPU 153→98 s). Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
vanilla_order replays vanilla's ticket scheduler over every chunk in view
each time the player crosses into another chunk, on the render thread. At
render distance 32 that is about 200,000 level updates in SipHash maps:
8 ms per call. Over dense grids covering the square it can touch, the
same replay takes 1 ms. A test checks the order against the hash map
version for several centres and radii.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content mod's data/<ns>/worldgen/ files were copied into the content root,
where nothing reads them: the worldgen loader reads bundles. The build now
adds a mod's structures, structure sets, template pools, processor lists and
configured and placed features to their bundles, its data/<ns>/structure/
templates to worldgen/structure/<ns>/, and its biome tags to the biome tag
bundle. The loader refuses a biome tag naming what it lacks (another mod's
biomes, a loader's convention tags), so those entries are dropped, as they are
from block and item tags. Biomes, noise and density functions stay out: they
would change vanilla's terrain.
Structure and placement types Horizon does not have stay unsupported, as
before; carry-over maps the common mod ones next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
26.3's copper and iron vein rules both bind minecraft:overworld/ore_vein/richness
eagerly, so the material stage sampled the same root over the same chunk volume
twice. Bindings of one compiled root now share its buffer; sampling is
deterministic, so the values are identical.
The two vein density reads per solid block went through
DensityVolume::index_of_block, which divides and takes the remainder by the
step on each axis. The stage's volumes are unit-step, so index them directly
(falling back for strided volumes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A remote client's chat now sends ServerboundCommandSuggestionPacket for an
argument node whose provider is minecraft:ask_server (bossbar ids, data
storage, datapacks, team names, functions...), as ClientSuggestionProvider.customSuggestion
does. The suggestions are done when ClientboundCommandSuggestionsPacket
answers that id; its completions join the local ones (Suggestions.merge).
Replies to older requests are dropped.
The remote tree takes each node's provider from the packet only: a local
provider the server didn't send stood in for the server's before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dedicated server draws nothing, like the integrated server, yet its
streaming still meshed every section it lit (Streaming::mesh followed only
options.integrated). It now skips meshing too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TitleCommand sends the targeted player ClientboundSetTitleText,
SetSubtitleText, SetActionBarText, SetTitlesAnimation and ClearTitles. A
guest's /title (the window's own player on the server thread too) only
changed the server's HUD; it is now sent to the guest. The remote client
showed titles and action bars as a bare line and ignored subtitles, times
and clearing; it now feeds the HUD's titles as ClientPacketListener does
(the action bar of a SystemChat overlay too) and ticks them (Gui.tick).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub held SetHealth back once a guest died, so the guest's health
stayed at its last positive value under the death screen. ServerPlayer
sends it every tick it changes, dead or not, and only when the health or
the food level changes or the saturation reaches or leaves zero: the hub
sent it on every change of saturation too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DistanceManager.PlayerTicketTracker gives every chunk within the server's
view distance (Chebyshev) of a player a PLAYER_LOADING ticket at level 31,
so FULL (33) reaches two chunks past it and FEATURES and TERRAIN one and
two more. A server player's streaming area was a circle of radius
(view distance + 2) * 3 / 2 + 1, finished one further: 2.1x the chunks at
view distance 12. It is now that square, and lan status reports the
server world's chunk counts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged vanilla-order block entity ticking (new harness: 21,590/21,590 ticks match vanilla; block-entity share of the tick 19.5%→9.6% with 50 bots; 90/90 harnesses locally).
Follow-up to #773 (consume effects on food):
- ApplyStatusEffectsConsumeEffect always draws its float and skips the
effects when the draw is >= probability (#773 skipped the draw at 1.0
and applied on <=).
- on_consume_effects run for every consumable, not only food: milk can be
drunk (clear_all_effects, then the bucket), honey removes poison and
leaves its glass bottle, stews their bowl (UseRemainder).
- The consumable's own timing, sound, animation and particles
(consume_seconds, sound, has_consume_particles) through horizon-world's
Consumable.emit, in vanilla's draw order; FoodProperties.onConsume's
sound and burp; the used statistic; the eat or drink game event after
the effects.
- teleport_randomly (chorus fruit) through horizon-world's verified
TeleportConsume port, with LivingEntity.randomTeleport for the player
(border clamp, entities_can_teleport_to ground, collision and liquid
checks), the teleport game event and sound, level event 2017's
directional particles and entity event 46's portal particles.
- tools/agent_control_e2e.py: a consume scenario (golden apple at full
hunger, milk, honey, chorus fruit).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CommandSourceStack.broadcastToAdmins beyond a guest on a dedicated server:
- The dedicated server's console tells every operator what its commands
did, as `[Server: ...]`, when `broadcast-console-to-ops` is on
(DedicatedServer.shouldInformAdmins). This covers the game's commands
and the dedicated-only ones vanilla broadcasts (op, deop, ban, pardon,
kick, whitelist, save-all, save-on/off, stop).
- In a world open to LAN, the host, an operator when commands are
allowed, hears of its guests' commands, and op guests hear of the
host's.
- A player's name in the line is their display name, with the `/tell`
suggestion, the entity hover and the insertion (Player.getDisplayName).
New harness test_server_ops_feedback_parity: two vanilla-codec
operators' system chat while the console and one of them run commands,
deop and op, toggle send_command_feedback and save, with
broadcast-console-to-ops on and off. LocatorBarClient can record system
chat (`chat on`). lan_e2e's feedback check also asserts the host is told.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a dedicated server, `execute in <dimension> run tp @s ...` from a player
moved them inside the level they were in. It now does what vanilla's
TeleportCommand does: ServerPlayer.teleport into the other level, with the
Respawn packet keeping all data and no portal sound, then the feedback. The
players there are tracked to them and they are removed from the ones they
left.
tools/test_server_dimension_players_parity.py joins two players to a vanilla
and a Horizon server and compares, step by step, the players each is sent and
loses and the dimensions it respawns into, while they go to the Nether and
the End by command and through an End portal. LocatorBarClient gains
`players on` and `still NAME` (the position reminder a still client sends).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`generate-structures=false` (server.properties, stored by #733) and the
world screen's Generate Structures were written to the world's settings
but the generator ignored them. VanillaTerrain now builds its generator
from the world's `generate_structures`
(OverworldGenerator::for_world): without structures there are no starts,
terrain adaptation or structure pieces, as in
ChunkStatusTasks.generateStructureStarts, and `locate structure` and eyes
of ender find nothing (ChunkGenerator.findNearestMapStructure).
The server world gen harness's world without structures now also
compares `locate structure` for a village and a mineshaft and the saved
chunks' structure starts and references.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Horizon client of a server places what PR #766 sends: primed TNT with
its fuse, falling blocks with their block state and start position,
arrows (critical, in the ground), snowballs, eggs, pearls, fireballs,
wither skulls, wind charges and llama spit, tridents, firework rockets
and end crystals with their beam and base. They interpolate like mobs,
turn with the move packets, and are cleared with the connection, a
respawn or a dimension change.
`status` lists them (`projectiles`), and lan_e2e has a `projectiles`
check: the guest sees the host's primed TNT, end crystal and landed
arrow where the host has them, and a falling block lands on both sides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`/summon minecraft:tnt` and primed TNT in a saved chunk were kept as an
unsimulated entity. They now become primed TNT with the fuse (80 when
absent) and motion they were saved with (PrimedTnt.readAdditionalSaveData).
The entity harness runs its scene for a fixed 200 ticks (`tick step`),
so what still changes when the client stops recording (a fuse) ends the
same on both servers. Its scene has two primed TNT (one falling), a
floating falling block, an arrow that lands and an end crystal. Unit
tests cover arrows' position and rotation, fireballs' bundled power and
precise position syncs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
These entities had no id in Horizon, so a server could not send them to
its clients. Each now has one, given from the entity counter before it is
first tracked, and goes to guests as vanilla's ServerEntity sends it:
- primed TNT with its fuse (sent each tick it changes) and falling blocks
with their block state and start position; both sync their exact
position while they touch a block (requiresPrecisePosition);
- arrows and tridents send their position and rotation every update
(AbstractArrow), with their flags, pierce level, in-ground state,
loyalty and foil; their owner is the spawn data;
- fireballs, wither skulls and wind charges bundle their velocity with
ClientboundProjectilePowerPacket (new in horizon-protocol);
- snowballs, eggs, pearls, llama spit, shulker bullets and splash potions;
- firework rockets with their item, the player they boost and whether
they were shot at an angle; end crystals with their beam and base.
The entity harness's scene gains primed TNT, a falling block, an arrow, a
snowball and a trident that land, a fireball, a firework and a crystal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleAddEntity for the objects a server tracks: a
Horizon client of a server (a LAN host, a dedicated server, single
player's server thread) now places the boats, minecarts, armor stands,
item frames and paintings it is sent, where the server says.
- Spawn: carts by kind, boats and rafts by wood (with a chest), armor
stands, frames and paintings at their block with the direction from the
spawn data.
- Entity data: frame items and rotation, painting variants (a registry
holder), armor stand flags and health, boat paddles, vehicle hurt.
- Equipment: what an armor stand wears and holds.
- Moves interpolate like mobs'; rotation and ground state follow.
- They are cleared with the connection, a respawn or a dimension change.
`status` lists them (`objects`), and lan_e2e has an `objects` check: the
host summons them, the guest sees each as the host has it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server sent its clients only mobs and dropped items. Boats, every kind of
minecart, armor stands, item frames, paintings and the entities Horizon keeps
without simulating are now tracked as ServerEntity does: the AddEntity data
(hanging entities at their block, with their direction), the synched data that
differs from the defaults, equipment, passengers (ClientboundSetPassengersPacket
at pairing and on change; a passenger syncs only its rotation and resyncs its
position when it dismounts), and the type's update interval and range.
Also vanilla-exact on the way, as the new harness found:
- mobs pair with their health (always non-default) and Mob flags (no AI, left-handed);
- /summon without NBT summons any type from an empty compound;
- armor stands move before gravity (LivingEntity.travelInAir), so a stand at rest
keeps vanilla's -0.0784 vertical motion.
tools/test_server_entities_parity.py compares, from a vanilla-codec client, what
vanilla's dedicated server and Horizon's track for a scene of 20 entities.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
scan_for_hearts read all 4,096 blocks of every non-air section of each new
chunk on the server thread to find creaking hearts. A section whose palette
holds no heart state (PalettedContainer.maybeHas) cannot have one, so it is
skipped. Same block entities, same order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged the six LAN guest fixes for #696 (#722–#727), combined on main and checked locally (clippy, tests, parity lan/dedicated_server/mob_ai, lan_e2e clicks/sleep/push/eat/xp).
These entities were drawn every frame wherever they were in the loaded
world: every armor stand, item frame and painting of every loaded village
or base went through its model, item and per-vertex occlusion test even
far behind the camera. EntityRenderer.shouldRender now decides, as for
mobs and minecarts:
- Entity.shouldRenderAtSqrDistance from the current position: 64 times the
bounding box's mean edge times the entity distance option (boats about
71 blocks, primed TNT 63, paintings by size); item frames 1024 blocks;
armor stands 256 times the box's mean edge whatever the option (4 for a
marker's empty box); tridents ten times the default (AbstractArrow);
firework rockets 64 blocks.
- the interpolated culling box grown by 0.5 in the view; tridents grow it
by 1.5 more (ThrownTridentRenderer), armor stands with a dragon head by
0.5 (LivingEntityRenderer), item frames holding a map use the block-wide
box.
The fishing bobber is unchanged: vanilla never frustum-culls it
(FishingHookRenderer.affectedByCulling) and Horizon only draws the
player's own, always within its 64 blocks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The section occlusion search (SectionOcclusionGraph) ran on the render
thread whenever a section was meshed or the camera entered another
section: at render distance 32 that is a breadth-first walk over about
124,000 cells, and while exploring it ran nearly every frame.
As in vanilla 26.3, the full search now runs off the render thread. A
worker keeps its own copy of every section's face connectivity, fed the
changes with each job, and searches over dense arrays around the camera.
Frames use the last finished graph and never wait for the next. The search
starts at the top of the frame, as soon as the camera is known, so it
usually lands before the frame builds its visible list. A section meshed in
between propagates into the graph the frame has (vanilla's
runPartialUpdate), so a wall that opens shows what is behind it at once; a
section that closes goes at the next full search. A graph that never
reached the camera's section (after a teleport, say) is not used: until the
next one lands every section in the frustum is drawn.
The search itself is unchanged: the same box, steps and first-entry rule,
checked cell for cell against the previous code in a test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
light_chunk's sky and block spreads read LightTables::cells for every
neighbour, and the fill and both spreads dominated lighting. Pack each
cell into two bytes (occlusion type and dampening) with a per-type
blocking mask, step neighbours by linear offset with an axis bounds
check, and precompute the per-column present table.
1156-column probe: 1.81 -> 1.31 ms/col (~28%). Vanilla light parity is
unchanged (1,179,648 sky/block values, 0 differences).
entity_motion_events looked up each moving mob in a SipHash map every
tick. The keys are mob ids the server gives out, so the fast hasher is
safe there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Their particles, ambient sounds and the turning display mob are for a
window. A dedicated or integrated server has none, yet made a display
mob for every spawner it ticked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The list kept its slots for the whole tick; sweeping it every tick cost
two hash lookups per ticker. Now it is swept only when a slot was freed
or a block entity was taken out and not brought back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockSection::get and set check the section index and the state id once,
then read and write through new PackedStorage::read/write, which skip the
storage-level bounds and value checks and the Result that get/set keep for
external callers. A section index is below 4,096 and a palette id always
fits the width bits_for_palette chose; both methods debug_assert that
contract. Used for the hot read, the hot write and the growth re-encode.
Benchmark (ignored tests, best of alternating-order rounds, fast profile):
BlockSection::set 13.15 -> 9.12 ns/write (1.44x), get 2.33 -> 1.45 ns/read
(1.61x). See docs/performance/paletted-container-access-results.md.
World::block_entities keeps vanilla's Level.blockEntityTickers beside the
map: a slot when a block entity whose state has a ticker is added, kept
when it is replaced, freed when it is removed, unloaded or its state loses
the ticker; a loaded chunk's in its block entity map's order. The tick
visits that list once, in block-ticking chunks within the world border,
instead of scanning the whole map once per kind.
New harness block_entity_tickers: real LevelChunks of a server level
against BlockEntities.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged by Ludovic's AI agent. CI parity passed (harness paths stay synchronous). Smoke test of the new path on current main (macOS, --offscreen, normal launch): resources loaded on the worker in 9.3 s, renderer swapped, title screen up and running for 60 s, no panic. Thanks @rdbtCVS!
The NBT tables stream, the fluid tables reuse the game's facts and the content
root is kept between starts (#753, #754, #756). Horizon with ATM10's 40 data-tier
mods against the same mods on NeoForge 21.1.259. Worldgen carry-over and what it
does not cover yet (#763, #764). Chipped's harness row with its self-drop tables
(#762). The overlay cache line said it was cached; it is now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockSection::set resolved the old cell through get and then probed the
section palette three times per non-trivial write: keying the old state by
id, testing the incoming state and finding it. The slot at the written index
already is the old state's id, so read it straight from storage and resolve
only the incoming state, with a single map probe. Palette reuse, free slots
and growth are unchanged, so palette, counts, free list and NBT are identical.
PalettedContainer::unpack and unpack_u16 matched the palette variant inside
the per-cell loop; dispatch Single/List/Global once before it.
Measured against copies of the pre-optimization code (ignored tests):
BlockSection::set 12.73 -> 11.37 ns/write (1.12x), unpack list 1.956 ->
1.860 us/call (1.05x), global 2.255 -> 2.172 (1.04x). See
docs/performance/paletted-container-access-results.md.
set_matches_baseline_under_random_edits drives both implementations over the
same 50k-edit streams for 1/2/16/17/24/64 distinct states, with periodic
whole-section collapses, and asserts equal results, bits, palette, counts,
free list and NBT.
Structure mods build on libraries whose structure, placement and pool element
types are vanilla's with extra rules: Lithostitched's, Moog's Structure Lib's
and Repurposed Structures' jigsaw structures, their advanced random spreads,
versioned and ocean-bottom single pool elements. Carry rewrites them as
vanilla's types; the extra rules (no start in liquid, a minimum distance from
the origin) are lost, so a structure may start where the mod would not. Types
with no vanilla counterpart (YUNG's, Formations', the nether variants) are left
as they are and counted in the carry report.
The structures' biomes name the loader's convention tags (#c:is_plains). fetch
now also gets the pack's NeoForge jar from NeoForge's Maven repository, checked
against its SHA-1, and carry copies the convention biome tags each mod's
structures use, with those they include, into the mod.
ATM10's 42 carried mods: 1,061 files rewritten, 103 convention biome tags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chipped registers its blocks' loot in code: its jar ships 7 loot tables for
about 7,000 blocks, so carried over they dropped nothing, while in the NeoForge
oracle every one drops itself. When a declared block has no loot table in the
jar and the oracle shows vanilla's dropSelf (one of itself with every tool,
nothing only where the state needs a tool it did not get), carry writes that
table in 26.3's shape. When only the states with one property value drop it
and the rest drop nothing (a door's lower half), the entry gets vanilla's
match_block condition on that value, as createDoorTable writes. Chipped:
6,843 tables added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The nether-portal section precheck from #661 answered "does this section's
palette hold a portal?" per section, per tick. Keep the answer per resident
chunk in `portal_sections`, keyed by a weak handle: an edit or a chunk
replacement rebuilds it, and it is dropped when a chunk unloads and cleared
when the block facts change, so a portal block always forces the sweep. The
entity sweep now finds portal blocks without the per-tick map.
This is a scan microbenchmark, not a whole-game claim: steady improves 2.4x
to 17x over the fixtures and the run parity-checks the cached and per-tick
answers. Rescanning every chunk every tick (cold, reload, edit_all) is slower
because a miss reads all 24 sections of a chunk; see
docs/portal-scan-benchmark.md.
Halves the CPU of deflating a chunk column and makes small packets about
20 % cheaper. The compressed bytes change; what they inflate to doesn't.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
getFiddledDistance restarts a six-step LinearCongruentialGenerator mix for
each of the eight quart corners. Its first two steps depend only on the X
and Y corner offsets, so quart_at_block now mixes the two X states and the
four XY states once, then runs the remaining steps across the eight corners.
next(m, salt + offset) == next(m, salt) + offset, so a coordinate folds into
its step with one addition.
Bit-identical to BiomeManager.getBiome and getFiddledDistance; a reference
oracle test pins the equivalence over a dense grid and wrapping extremes.
🤖 Agent: merged by Ludovic's AI agent: perf-server-5. Before/after in the PR body; agent parity 87/87 stacked; orchestrator combined check with the perf-ticks-5 stack on current main: clippy clean, client+protocol tests pass (one flaky world-delete test, #749), dedicated_server/lan/chunk_stage/block_entity_storage PASS.
🤖 Agent: merged by Ludovic's AI agent: perf-server-5. Before/after in the PR body; agent parity 87/87 stacked; orchestrator combined check with the perf-ticks-5 stack on current main: clippy clean, client+protocol tests pass (one flaky world-delete test, #749), dedicated_server/lan/chunk_stage/block_entity_storage PASS.
resource_root built the content root again on every start. With ATM10's 40
carried data-tier mods that is about 25 s on a quiet machine (50 s on a busy
one) and the biggest memory peak of the start, above 3 GB of footprint. The
root now records a stamp of what it was built from (this build of the game,
the base's manifest and rewritten tables, every mod file by size and time) and
later starts with the same stamp use it as it is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FluidResources decoded gameplay-facts.json a second time for three per-state
columns the game's BlockFacts already has. With ATM10's carried mods that file
is 211 MB: the second decode took a few seconds and peaked about 500 MB above
what the game keeps. MotionResources::load_with_facts takes the game's facts;
load still decodes them itself for tests and tools.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged by Ludovic's AI agent: verify passed, parity proven (CI or perf-frame-2's local run), before/after numbers and screenshots in the PR body.
🤖 Agent: merged by Ludovic's AI agent: verify passed, parity proven (CI or perf-frame-2's local run), before/after numbers and screenshots in the PR body.
🤖 Agent: merged by Ludovic's AI agent: verify passed, parity proven (CI or perf-frame-2's local run), before/after numbers and screenshots in the PR body.
The title's GUI atlas and its panorama both come from the resource root and
are only needed to draw the title and menus, but they lived apart: the atlas
on Game and the panorama inside PackResources. Gather them into UiResources so
Stage 2 can load the title bundle before the world bundle. No behaviour change.
Part of #712 (stage 2a).
A Horizon game with content mods builds its content root and block behaviour
snapshot on its first start, in its game folder, and reuses them afterwards.
perf_bench starts every game from an empty folder, so it only measured first
starts. --warm starts the game once to the title screen first, unmeasured.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged by Ludovic's AI agent once CI parity passed; screenshots identical to main (perf-frame-2), CPU frame time about 40% lower on a chest field.
occlusion.nbt, cached-collision.nbt, cached-support.nbt, block-properties.nbt and
movement-properties.nbt each carry the identity of every block state, and every
loader parsed them into an NBT tree first: 240 MB per table for ATM10's 153,511
states, about 1.4 GB allocated and freed while the game starts.
nbt::read_named_streaming hands a named list to a visitor element by element.
registry::BlockTableFile reads a table that way, checking each state against the
registry as it comes, and loaders build from a checked registry::BlockTable.
from_nbt stays for trees built in memory (tests, fixtures).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each player's writer thread compressed every packet it sent, so a chunk
column sent to fifty players was deflated fifty times; compression was
most of the writer threads' time and about half of the server's CPU.
Bodies of 8 KiB or more (chunk columns, mostly) now go through a shared
cache of compressed frame contents keyed by their length, two
independently keyed 64-bit hashes and the threshold, holding the newest
32 MiB. The bytes on the wire are the same: frame::encode is split into
frame::contents and frame::prefixed, and ConnWriter::send_contents
sends a frame from its contents.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The synchronous startup loaded every resource before the first window
existed, so the game hung on a blank screen for seconds. An ordinary
launch now enters the event loop at once, opens the window, and draws a
loading screen with the current stage and a progress bar while a
resource-loader thread runs the existing load sequence. When it finishes,
audio is created on the render thread and the real renderer, game and
title screen replace the loading screen on the same window.
Harness, dedicated and scripted runs (--frames, --world-frames,
--benchmark, --control, --dedicated, HORIZON_MENU_STYLE=classic and the
parity fixtures) keep the fully-synchronous path unchanged, so their
output stays byte-identical. title_shown still fires on the real title,
not on the loading screen.
🤖 Agent: merged by Ludovic's AI agent once verify passed (the earlier failure was a flaky client test #721 doesn't touch; main passes it). Launcher tests run locally on main + #719 + #721: all pass. Thanks @rdbtCVS!
🤖 Agent: merged by Ludovic's AI agent. CI doesn't build the launcher on PRs, so launcher tests were run locally on main + #719 + #721 (cargo test --locked, macOS): all pass. Thanks @rdbtCVS!
Six scenes from one fixed-seed world (seed 8675309): a plains village among
savanna hills, sunset over it and the river, moonrise over its lit square, a
crimson forest with a zombified piglin, tropical fish over a warm-ocean coral
reef, and survival at dusk with a zombie and the full HUD. Plus Horizon's own
title, Singleplayer, Video options and pause screens.
Captured with horizon-ctl screenshot from the offscreen client (fullscreen
2560x1286 on the Mac mini, default graphics settings, GUI scale Auto), scenes
cropped to 16:9 and menus to 16:10 (left-aligned) and scaled to the old sizes
(1600x900, 1280x800). The scene sources are JPEGs in docs/media again, the
names scripts/assets.mjs already reads; the README's WebP copies are the
rebuilt website media. Alt texts and LEGAL.md follow the new pictures.
The launcher screenshots are unchanged: the Mac mini's session is locked, so
no window capture works.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged by Ludovic's AI agent. CI parity passed on the head; combined with current main (and #680) locally: workspace clippy clean, horizon-client 826 + horizon-world tests pass, mob_ai/save_roundtrip/spawner PASS. Thanks @Online5277!
A Prism or MultiMC instance keeps no jar of its own: the client sits in the
launcher root's shared libraries, two or three folders above `instances/`,
and the game is downloaded only once the instance is launched. Choosing
`%APPDATA%\PrismLauncher\instances` (or an instance's `minecraft`/`.minecraft`
folder) as the Minecraft folder failed Prepare when the jar was not there yet,
and the error only listed every launcher without saying what to do.
resolve now also looks in the launcher root above an `instances` folder, so a
folder deeper than the four walked above the chosen one still finds the
libraries. When nothing is found and the chosen folder is inside a launcher
Horizon knows, the error names it and says to launch the instance once so it
downloads the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged by Ludovic's AI agent after local checks on a6592eb1: workspace clippy clean; horizon-client/world/protocol tests pass; lan, chunk_stage, dedicated_server, block_entity_storage, chunk_generation PASS.
Settings gains a one-line Game arguments field, kept as `game_args` in
settings.json and appended to the client's command line after the
launcher's own, so a player can pass things like `--server-thread` or
`--render-distance 40`. Quotes group a word with spaces in it.
`horizon-launcher game-args [<line>]` shows or sets the line,
`play --arg <ARG>` passes one for a single run, and the MCP launch_client
tool takes an args array. The same splitter feeds all three.
Closes#716.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_hoppers read the block under every plain container block entity
each tick (chests, barrels, shulker boxes, dispensers, droppers) to find
the hoppers. A hopper's block entity is the only five-slot one
(BlockEntity::for_block, and BlockEntity::id already names it the
hopper), so only those are looked up now; a position still needs a
hopper block to tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a LAN world or a server, guests never got experience: orbs followed and
were taken only by the host's own player (none on a dedicated server), and
guests weren't sent orbs at all.
- Each orb follows the nearest living, non-spectator player within 8
blocks, the host or a guest (ExperienceOrb.followNearbyPlayer).
- A guest takes an orb as Player.aiStep does: one of those in its box grown
by 1, 0.5, 1, at random, at most every 2 ticks (takeXpDelay), repairing
with Mending first; the rest goes to the guest's experience (sent with
SetExperience as before), the level-up sound every fifth level.
TakeItemEntity goes to the players around, the pickup sound plays at the
orb.
- Orbs have entity ids and are tracked to guests (AddEntity, DATA_VALUE).
The client shows a server's orbs, moves them with the server's packets and
flies them to their collector.
tools/lan_e2e.py xp: three 7-point orbs near the guest: it sees three and
ends at level 2 (21 points); the host gets none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mob's push of a guest went to the guest as SetEntityMotion carrying the
server copy's velocity, which nothing damps: every tick of contact added
to it and replaced what the guest's client was doing. Vanilla pushes
players on their own client only (EntitySelector.pushableBy): the server
no longer sends those pushes, and a server client now pushes its player
away from every living entity it touches (Entity.push, team rules, no
spectators, bats or passengers of the same vehicle).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minecarts were drawn every frame wherever they were: at render distance 32
every chest minecart of every loaded mineshaft went through the rail
lookups, display block and model, and the per-vertex terrain occlusion
test. Vanilla draws a cart only within 64 times its bounding box's mean
edge (about 57 blocks at entity distance 100%), measured from its current
position, and only when its interpolated box, grown by 0.5 and stretched
up by the display block's offset, is in the view
(EntityRenderer.shouldRender, AbstractMinecartRenderer
.getBoundingBoxForCulling). The cart the player rides always draws.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a LAN world or a server, other entities' heads flickered between two
directions. The client wrote ClientboundRotateHeadPacket's head yaw into
the entity's yRot and the move packets' yRot into the body yaw, never moved
the head's previous yaw and pitch on, and never ran the body rotation, so
each frame interpolated the head from stale values.
As vanilla: move packets interpolate the position, yRot and xRot over three
ticks (LinearInterpolationHandler), RotateHead the head yaw over three
(lerpHeadTo), each tick keeps the previous head rotation for the frames, and
the body follows (a mob's BodyRotationControl, a player's walk direction
within 50 degrees of the look). AddEntity sets yRot and xRot and puts the
body and head at the packet's head yaw (recreateFromPacket).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a LAN world or a server, a guest's clicks that went through the menu
engine (placing a carried stack, quick craft drags, double clicks, number
keys, Q) ran only on the guest's own copy and were never sent: the item
showed in the chest or furnace, the server still had it in the inventory,
and the next menu brought it back.
menu_ops now sends every click of the server's open menu as vanilla's
MultiPlayerGameMode.handleContainerInput does: the engine predicts it on
the client's copy, then a ContainerClick goes out with the slots it changed
and the carried stack after it (a drag is its three kinds of clicks). The
plain inventory and crafting table are predicted too, the Shift double click
sends each QUICK_MOVE, and a prediction no longer drops, crafts or makes maps
on the client (the server does).
tools/lan_e2e.py clicks: real mouse presses put a stack into a chest and a
furnace, drag planks over the crafting grid and craft; the server has each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
At the end of each tick the game thread handed every player's held
packets to that player's writer thread, waking each one: a system call
per player, and on a busy machine often the rest of the core's time
slice to the woken writer. Now the batches go to one post thread in a
single send, and it wakes the writers. A packet sent to a player while
its batch is still with the post thread goes the same way, so each
player's packets keep their order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkEncoder::body_in writes the LevelChunkWithLight body that
Clientbound::encode would give, into one buffer sized up front: the
cached section bytes and light layers are copied once, uniform light
layers are filled in place, and the heightmaps are shared rather than
cloned. Before, each chunk sent built a packet (growing its buffer,
cloning every 2048-byte layer), then encode copied all of it again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server client sent UseItem and never started the use itself, so food
showed no eating animation or crumbs, and holding right click sent a new
UseItem every 4 ticks. Like MultiPlayerGameMode.useItem, the client now
starts eating (Consumable.startConsuming), counts the use down for the
animation and its particles, and starts no other use until it ends; the
server still eats the item. Looking at another player no longer stops
the item from being used (Player.interactOn passes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bed's sleeping state, timer and respawn point were the host's fields,
so a guest clicking a bed put the host to sleep and moved the host's
spawn. as_guest now swaps them with the guest's own; the guest gets its
sleeping pose and bed (entity data 14), wakes on STOP_SLEEPING with the
WAKE_UP animation, and the night skips only once enough players sleep
(players_sleeping_percentage). Guests respawn at their own bed, else the
world spawn. Server clients lie in the bed from the synced position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu replays an indirect multi-draw on Metal as one draw call per entry, so
every frame encoded a draw per visible face range and translucent section
(9,300 at render distance 32). Where multi-draw is emulated (Metal) and a
frame has at least 4,096 section draws, a compute pass now copies the
frame's section draws into one index list, in draw order, and each run of
sections in the same arena page is one draw call: 9,309 draw calls become
about 750 at RD32. Smaller frames keep the per-section draws, where the
list's GPU cost (about 0.4 ms on an M4 Pro at RD16) outweighs the CPU saved.
The picture is unchanged by construction: the same triangles from the same
vertices in the same order through the same shader code. Only where a
vertex finds its section's origin changes, from the instance attribute to
a table: a section's vertices fill whole 256-vertex blocks of its page, each
block names the section, and the section's camera-relative origin is
computed on the CPU exactly as before.
Vulkan keeps the native multi-draw. HORIZON_FLAT_DRAWS=1 draws every frame
flat (any backend that supports it), 0 never; the control command
`section-draws flat|indirect` switches in a running game for A/B checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apple GPUs time only pass boundaries (no TIMESTAMP_QUERY_INSIDE_ENCODERS),
so the GPU timer was off on every Mac and benchmarks reported no GPU time.
Where only pass boundaries can be timed, the frame is now bracketed by two
empty compute passes that write the timestamps.
HORIZON_SPIKES also names the renderer's parts (occlusion, visibility,
encode, submit), and the status reply counts the frame's section draws and
the draw calls that submit them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TickCtx gained `collision` here; main's two newer initializers
(lan_actions respawn, server_travel's server_ctx) now pass it too, as
every other one does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DedicatedServerProperties.WorldDimensionData: a world that doesn't exist
yet gets level-type's preset (flat or normal; an unknown name warns and
falls back to minecraft:normal), a flat world's generator-settings
(FlatLevelGeneratorSettings: layers, biome, lakes, features; a layer that
doesn't parse is logged and left out, and settings without layers use the
preset's classic ones) and generate-structures, written to
world_gen_settings.dat before the world opens, with vanilla's log lines.
Horizon ignored all three and always made a normal world.
New harness server_world_gen compares world_gen_settings.dat and the lines
logged with vanilla's for five server.properties.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/give, /clear, /gamemode, /experience, /advancement, /playsound and the
whisper-to-yourself lines printed the host's placeholder name for the
acting player. They now use the acting player's name (a guest's own while
their command runs), as target_name and the scoreboard do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While a guest's command or action runs, the score holder, team, display
name and death and kill criteria of the acting player use the guest's own
name (ScoreHolder.getScoreboardName), as on vanilla's server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CommandSourceStack.broadcastToAdmins: on a dedicated server, a guest's
command success (sendSuccess with broadcast) is logged on the console as
"[Name: message]" when log_admin_commands is on, and sent gray and italic
to every other operator when send_command_feedback is on. The queries and
lists vanilla sends with broadcast false (commands/admin.rs) stay with the
player who asked. Lines are queued while the guest acts and sent once the
command returns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ditto without --norsrc --noextattr --noqtn stores an AppleDouble ._ entry
beside every file that has extended attributes, so the macOS release zip
held hundreds of them (horizon-macos-arm64/._horizon-ctl, tools/._*.py).
Package it as packaging/macos/package.sh already does, and fail the job if
any ._ or __MACOSX entry is left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (the integrated server thread and the dedicated server)
the host has no player of its own, so effects that only touched the
host's player reached nobody; on a LAN world they reached only the
host. Beacons, the warden's and shriekers' darkness, the elder
guardian's mining fatigue, splash and lingering potions' splash and
the raid victory's Hero of the Village now run for each player in
the level, guests through `as_guest`.
The elder guardian's curse sends the guest the GUARDIAN_ELDER_EFFECT
game event, which the remote client now shows (the apparition and
the curse sound).
lan_e2e.py gains an `effects` check: a speed beacon next to both
players must give both of them speed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With an explicit Present Mode (Mailbox, Immediate), the VSync option did
nothing: the setting replaced vanilla's choice even with VSync on, so a
window kept running uncapped. Vanilla's VSync means Fifo, in a window and in
fullscreen alike. Now VSync on always presents with Fifo, and the Present
Mode setting chooses only while VSync is off (Auto keeps vanilla's choice:
Immediate, else Mailbox, else Fifo). The choice is one tested function,
window::chosen_present_mode.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The LAN chunk encoder's section cache (#623) and dimension range (#668)
are kept; a cached section's wire bytes use the registry's palette width,
set once per process before any encoder exists, so the cache stays keyed
on the section alone. The sculk scan's palette check takes the u32 state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LAUNCHER_PATHS and launcher.yml's push paths now list horizon-bench,
horizon-friends, horizon-mods and horizon-port-pack too, the launcher's
path packages per cargo metadata. A change there (like the horizon-mods
dependency that left launcher/Cargo.lock stale) then builds and releases
the launcher instead of going unnoticed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-port-pack now depends on horizon-mods (1f1d5a5a), which the
launcher's own lock file didn't have, so every --locked build of the launcher
failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server (the dedicated server and single player's server thread) now
holds a level per dimension, like vanilla's `MinecraftServer.levels`. Each
server tick runs every level with players in it, one at a time, by swapping
its fields into place (game/levels.rs), so the world tick code runs
unchanged. Before this, a portal did nothing while two or more players were
online.
- Levels: the parked levels keep the world, streaming, save, block ticks,
entities, block entity state, raids, sculk and creaking scans. The
Overworld stays loaded; another level is saved and closed after 300 ticks
with nobody in it. Saving saves every level.
- Hub: `tick` is split into `poll` (the network, once per tick),
`tick_level` (the packets, chunks, block changes, containers and entities
of the players in the level in place) and `end_tick` (latency). Each guest
has a dimension; time, weather, sounds and events reach only the players
in their level, and the clock and weather last sent are kept per level.
- Travel: every player has their own portal state; a portal moves only that
player into the destination's level (`ServerPlayer.teleport` packets as
before). Respawning brings a player back to the Overworld's level.
- Player data saves and loads each player's dimension, and a returning
player is placed in their own level.
- tools/lan_e2e.py --server-thread --check portals: the guest goes to the
Nether while the host stays, then the host goes to the End while the guest
is in the Nether, and both come back.
The in-frame game keeps its one level that follows its player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Windows the launcher links another launcher's client jar with a hard link
(a copy across volumes) and its libraries and assets with a junction, since
symlinks need developer mode. canonicalize() of a hard link is its own path,
so the sources tests, which compared canonical paths, failed on Windows and
blocked every launcher release since launcher-v0.4.0. They now check that the
linked jar is the launcher's file (a byte written through one shows in the
other), which holds for a symlink and a hard link alike.
The modpack profile test compares paths as paths rather than strings ending
in "a/carry".
mklink /J's paths are always quoted: Rust only quotes arguments with spaces,
and cmd reads an unquoted & or ^ in a user folder as its own syntax.
CI runs every launcher test target, not just the library's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first criterion trigger of a world's first tick loaded the
advancement tree: about 1,400 JSON files read and parsed on the game
thread, about 60 ms of a 125 ms first tick. open_world now starts that
read on a thread, and the first advancement_tree() takes its result,
the same tree as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's action runs on the host's game with the guest's state swapped in
(`as_guest`). The swap restored the open screen but not what opening one
does to the window: `capture(false)` released the host's pointer, so the
cursor showed and the camera stopped whenever a guest opened a chest, a
crafting table or any other menu. `keys.clear()` dropped the keys the host
was holding, and a sign, book, command block or mount menu the guest opened
was drawn on the host's screen.
- `capture` leaves the window alone while a guest acts.
- `as_guest` keeps the host's held keys, sign editor, book, command block
screen and mount menu, as it already kept the open screen.
- `state` reports `pointer_grabbed` (mouse movement turns the camera).
- tools/lan_e2e.py: `chest` and `crafting` check that the host keeps its
screen and its mouse look; a new `sign` check places a sign as the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub tests every player's sent-chunk set for every entity and every
column of the view each tick, and each player's tracked-entity map for
every entity: tens of thousands of SipHash lookups a tick with 50
players. Both now use the Fx hash. Their iteration order was random
before and carries no meaning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every chunk packet scanned all 4,096 blocks of each section holding a
block entity block to list them, though the section's wire bytes were
already cached. The cache now keeps those blocks (index and state, in
scan order) beside the bytes, made in the same pass that encodes them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
encode_in asks whether each of the nine surrounding columns' 24 sections
holds anything, to decide which light layers exist: 216 cache lookups,
each locking the cache and SipHashing a section's address. The column's
sections now share one lock, and the cache uses the Fx hash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub looked up an entity's tracking range (a binary search over type
names) for every player and every entity each tick: about 87,000 string
comparisons a tick with 50 players. The ranges are now worked out once
per entity per tick; the visibility test is the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The generation scan walked every area's square of columns, so with 50
players standing near each other it tested about 100,000 columns a tick
(each against every player's range) to find the few hundred around them.
The squares are now merged into one run per row first. The candidates
are the same: they were sorted and deduplicated before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_hoppers read the block under every block entity in the loaded world
each tick to find the hoppers. A hopper's block entity is always a plain
container (BlockEntity::Chest), so only those need the lookup.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerAdvancements.flushDirty only works when it is the first packet or
progress changed. Horizon walked the whole advancement tree for every
network player every tick to find nothing new: a fifth of the server
tick with 50 players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Online mode checks accounts since v2026.10.17.07; until #589, players added
before they join get offline UUIDs, with a workaround. Portals follow a
lone player since v2026.10.17.09. Copied game files need v2026.10.17.07 or
newer. Section 10 links the 2026-10-17 benchmark and says plainly that 25
moving players are too many for now. Links to other docs are absolute, so
they work both on GitHub and on the site.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-bots and tools/server_bench.py at 1, 10, 25, 50 and 100 bots,
walk and explore, Horizon (main at 123987c4) and vanilla 26.3 taking turns.
The Mac mini was overloaded by other work (load 55-141), so only the 1-
and 10-bot rows are a fair comparison; the README says so and gives the
load of every run. It also records where Horizon's server thread goes at
25 walking bots: 96 % in ChunkEncoder::encode_in.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- perf_bench.py samples the physical footprint on macOS as well as
RSS. RSS counts the pages the allocator freed and marked reusable,
so the six mods' "+1.1 GB" is about 0.5 GB of footprint.
- examples/resource_memory.rs: live heap, peak, RSS and footprint after
each resource loader, for a prepared or a content-mod root.
- BlockRegistry indexes ids by a hash of the state, checked against
the states, instead of keeping every state twice: -22 MB live for
vanilla, -124 MB for ATM10's 42 mods (193,762 states).
- docs/modpacks.md: the per-loader breakdown and the next cut (the
per-state tables parsed whole at startup).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the wide state ids, ATM10's 42 data-tier mods (193,762 states)
reach the world once these loaders stop refusing them:
- Block sprites: eight texture arrays of 2048 layers instead of four
(9,790 block textures for the 42).
- Per-state tables read as NBT: the table limits cover 2^20 states, and
the collision table in level data uses them.
- Worldgen block tags: 2^20 blocks instead of 8,192 (mineable/pickaxe
holds every stone block of a pack).
- As vanilla's loaders do, a blockstate definition that does not bind,
a missing or broken block model of a mod, and a recipe that does not
parse are logged and skipped instead of dropping every mod (or
stopping the game).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chunk sections keep block states as u32 and size the global palette
to the registry: 16 bits for vanilla's 35,723 states, wider when
content mods take the registry past 65,536. The content-mod limit
goes from 2^16 to 2^20 states.
The LAN host writes chunks at the registry's width, so with vanilla
(or mods within 16 bits) the wire is unchanged. Remote chunks are read
at the server's width, 16 from a vanilla server, and widened to ours.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What works on main today (install, game files, first run, server.toml, the commands, the web panel,
backups, services, Docker built by hand) and, plainly, what doesn't yet: offline mode only, no
published image, no secure chat or locator bar, the open PRs that are coming. The site renders the
guide into /server at build time, so it has one source. The CSP hashes now cover every page's inline
script, so the privacy page's theme script is allowed too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Walk and explore passes at 1 to 100 bots on the ThinkPad (bots on the Mac
mini), vanilla with the locator bar off, plus an explore pass at a rate both
servers keep up with. The README has the method, the tables, the profile of
what remains at 25 walking bots and the earlier runs; docs/server.md section
9 and the Scale row summarise it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Block collision scans (vanilla's BlockCollisions) are most of every mob's
move, and moves were the largest share of the world tick with 600 mobs.
Per cell and per hit, the scan did work vanilla's JIT makes cheap:
- the cursor divided its index by the box width and height for every cell;
it now counts x, then y, then z, as Cursor3D does (same cells, same order,
same budget);
- the unit cube test cloned the shared unit cube to compare identities
(`VoxelShape::is_block` compares with it in place), and built a
normalised box to test it against the entity box (`intersects_corners`);
- every non-empty block looked its state up in the contextual rule map
(scaffolding, liquids, powder snow); a flag in the scan table now says
which states have one;
- a moved shape decomposed its occupancy into boxes again; the moved
shape's boxes are its own boxes moved, the same values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon has no locator bar yet, so equal-work runs turn vanilla's off
(--gamerule locator_bar=false): its waypoint updates cost every move
a pass over every other player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bots acknowledge chunk batches with ChunkBatchSizeCalculator's
desired chunks per tick, from how long each batch takes to arrive,
instead of a fixed 25. A bot moves only while it holds the chunk it
stands in, as vanilla's client stops ticking its player in a chunk it
hasn't received, so no bot outruns a server's chunk sending. Each bot
keeps the chunks it holds, so the report separates chunks that entered
its view from chunks sent again while it still held them, and counts
the moves the server refused and the ticks it waited for its chunk.
server_bench.py runs one pass per behaviour (walk, explore), spawns
every bot on the world spawn itself (respawn_radius 0) so both servers
see the same paths, can run the bots on another machine (--bots-via,
--bots-bin, --connect) and the servers under nice, and reports new,
dropped and re-sent chunks per second, pull-backs and the server's
"moved too quickly" lines.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerChunkSender: one unacknowledged batch until the client's first
acknowledgement and ten after it, batches capped at the client's
desired chunks per tick (not twice that), and the quota reset to one
chunk whenever every batch is acknowledged.
ServerGamePacketListenerImpl.handleMovePlayer: a move further from
where the tick began (firstGood) than 100 blocks squared per move
packet (300 while gliding; the count falls back to 1 past 5 packets)
logs "<name> moved too quickly!" and pulls the player back, as the
player_movement_check and elytra_movement_check game rules allow.
NaN coordinates or non-finite angles and two position moves in one
client tick disconnect with invalid_player_movement, positions are
clamped to the world's limits, an unanswered teleport is sent again
after 20 ticks, and accepting a teleport runs the move it carries.
The move is then replayed against the blocks: a survival or adventure
player who ends more than 0.25 blocks short of the target (horizontally;
vanilla drops the vertical part) logs "<name> moved wrongly!" and is
sent back unless they were already stuck, and any move into a block
the old box did not touch (isEntityCollidingWithAnythingNew) is sent
back silently. Spectators and sleepers skip both. Angles wrap as
Mth.wrapDegrees does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve minecarts.rs against the resources split (#537): the block model
catalog now lives in self.pack. Fill MobData's new villager_hats and pois
fields in the sulfur cube render test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Releasing the pointer only ungrabbed it, so the cursor showed up wherever
the system had it: on Wayland, where the locked pointer last stopped (often
where the previous screen was closed), elsewhere wherever the hidden pointer
had drifted. Opening the inventory, a chest, the pause menu, chat, a sign or
a book could put it anywhere.
Game::capture now follows MouseHandler.grabMouse/releaseMouse: both move the
pointer to the window centre, and releasing also sets Horizon's own cursor
(what every screen hit-tests and draws hover with) to that point. On Wayland
the position can only be set as the locked pointer's hint, applied by the
compositor as the lock goes, so it is set before ungrabbing; X11, Windows
and macOS warp, so it is set again once the pointer is free. Releasing an
already released pointer does nothing, so screen-to-screen changes keep the
cursor where it is, as in vanilla, and nothing warps while the window is
unfocused (GLFW's glfwSetCursorPos does the same), so losing focus never
pulls the pointer back into the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 01:12:18 +02:00
1825 changed files with 219056 additions and 42604 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)) |
A Rust rewrite of Minecraft: Java Edition 26.3 built for one-to-one vanilla parity, checked against the game itself with differential tests.
Every subsystem matches vanilla in all 281 parity harnesses, on Linux and macOS: 100% of what the harnesses compare (see [Project status](#project-status)). The native client is playable on Linux, macOS (Apple Silicon) and Windows.
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## What changed in v2026.10.13.1
The latest release ([notes](https://github.com/ludovic111/horizon/releases/tag/v2026.10.13.1)):
- **Horizon's own menus:** a new title screen, world list, options and pause menu in the launcher's style, over a live frozen landscape. The vanilla look is still available: Options → Interface → Menu style → Classic.
- **Graphics settings, Simple and Advanced:** presets (Performance, Balanced, Quality, Ultra), or every rendering, quality, display and performance setting, each with a short description and its cost, plus a performance overlay.
- **Much faster rendering:** for example, at render distance 8 over a village, 363 → 2,409 FPS on an RTX 4080. Render distance 32 still stutters in places.
- **Fixed:** no sound on Linux; Linux builds that only ran on the machine they were built on; a missing DLL on Windows (the launcher no longer needs the Visual C++ runtime); memory growing and the frame rate dropping near villages (mobs were duplicated when chunks reloaded); resource preparation failing on Linux with "Missing … java-objc-bridge" ([#41](https://github.com/ludovic111/horizon/issues/41)); the hand and held item at high FOV; missing swing animations.
## Download
Horizon is downloaded only through **the Horizon launcher**: get it from [playhorizon.dev](https://playhorizon.dev) or from [the launcher's releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true) (the newest `launcher-v*` release, currently [launcher-v0.1.3](https://github.com/ludovic111/horizon/releases/tag/launcher-v0.1.3)), for Linux x86-64, macOS Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its resources from your own copy of Minecraft, starts the client or a dedicated server, and updates itself.
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
**You need** Minecraft: Java Edition installed with the official launcher, with 26.3 played once; Python 3.10 or newer and Java 25 (a JDK, e.g. Eclipse Temurin 25) for the one-time resource preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac.
### The launcher
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 JDK, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: v2026.10.14.1 ready to play, its summary, Client and Server, the version picker and Play, the resources and the Minecraft folder as a numbered list"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page, showing the notes of Horizon 2026.10.14.1"></td>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: a version ready to play, its summary, Client and Server, the version picker and Play"></td>
It lists Horizon's releases and installs the build for your platform, shows each release's patch notes, updates Horizon and itself, and doubles as a command-line tool and an MCP server. See [the launcher's README](launcher/README.md).
## What works today
## Screenshots
The [documentation](https://playhorizon.dev/docs/) covers each of these.
Captured in the Rust client itself, with vanilla assets and no mods or shaders.
- **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 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).
### Menus and settings
<table>
<tr>
<td colspan="2"><img src="docs/media/menu-title.webp" alt="Horizon's title screen: the mark and the pages on the left, the Horizon title, Singleplayer and Multiplayer, and the last world played and the faint wordmark, over a veiled snowy landscape"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/menu-worlds.webp" alt="The Singleplayer page: three worlds with their dates and game modes, and Create world and Play buttons"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="The Video settings in their Simple view: a preset, render distance, field of view, framerate, VSync and GUI scale, each with what it costs"></td>
</tr>
</table>
The vanilla menus are one setting away: Options → Interface → Menu style → Classic. See [Graphics settings](docs/graphics.md).
### In game
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village with farms, an iron golem and animals"></td>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie attacks near the village"></td>
<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 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>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
<td width="50%"><img src="docs/media/night.webp" alt="Moonrise over a torch-lit village and river"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/underwater.webp" alt="Salmon swimming in a river"></td>
<td width="50%" valign="middle">
Vanilla world generation ported to Rust (terrain, aquifers, carvers, features, villages and the Nether), every block model with smooth lighting and biome tints, the vanilla sky and fog, mobs, the HUD, chat and advancements: everything in these frames is drawn by Horizon.
</td>
</tr>
</table>
## What it does
## Host a server
Horizon reimplements the Minecraft Java client and its game logic in Rust, one subsystem at a time, and compares each piece with the vanilla game. Each `tools/test_*_parity.py` harness drives the same inputs through Horizon and through the vanilla client and server bytecode from your own copy of the game, then checks that the outputs are identical. It is for players and developers interested in game reimplementation, and for anyone who wants to see how far a behaviour-exact native rewrite can go.
The launcher's **Servers** page creates, starts, backs up and updates a Horizon server with buttons, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and runs without Java. Guide: [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
The [native client](docs/native-client.md) opens on a vanilla-style title screen with world creation and selection, and generates worlds with the Rust port of the vanilla generators: noise terrain, aquifers, surface rules, carvers and placed features, all checked against vanilla. It streams and lights chunks on worker threads and renders every vanilla block model with smooth lighting, biome tints, the vanilla sky, sun, moon, stars and clouds. Gameplay includes:
## Modpacks
- validated player physics, health and hunger;
- mining with vanilla tool rules and loot tables;
- placing, crafting, chests and furnaces;
- dropped items, particles and sounds;
- passive and hostile mobs with combat and vanilla AI, including guardians, shulkers, illagers, ravagers, the wither, the ender dragon fight, the breeze, the creaking, the sniffer, the happy ghast, llamas and the copper golem;
- villagers that take jobs, trade, restock, meet at the bell and sleep in beds, on a port of vanilla's brain;
- raids and pillager patrols;
- dispensers, crafters, the loom and the recipe book;
- every command, scoreboards and teams, the player list, subtitles and the HUD overlays;
- boats, minecarts, item frames, paintings and armor stands;
- shields, maps, books, fireworks, beacons, advancements and statistics;
- Anvil saves, joining vanilla servers, and opening a world to LAN.
The launcher's **Modpacks** page imports a Modrinth or CurseForge pack, shows which of its mods Horizon can carry over, and plays the ones it can as their own profile. Today that means content mods made of data and assets; mods with Java code are not ported yet. See [Modpacks](https://playhorizon.dev/docs/modpacks).
## Agent control
@@ -147,7 +107,18 @@ cargo run --release -p horizon-client
Everything prepared lands in the gitignored `.local/`. It is derived from the game: never commit it or share it.
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, playing and scripting through `horizon-ctl` and its MCP server, and the architecture are described in [the native client guide](docs/native-client.md). The rest of the documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, scripting through `horizon-ctl` and the architecture are described in [the native client guide](docs/native-client.md). The rest of the developer documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels), or [the contributing guide](https://playhorizon.dev/docs/contributing) on the site. Questions and ideas: [Discord](https://discord.gg/HcTeMZxe5E). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a maintainer's computer holding their own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
## Project status
@@ -182,18 +153,7 @@ them too (Rust uses Java's own float maths on each platform).
### Known issues
Online-mode servers (Mojang's session servers) are not supported; skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a self-hosted runner holding the maintainer's own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
Horizon can't sign in to a Microsoft account yet, so it can't join online-mode servers (it joins offline-mode ones); skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 15.1 / 120 |
Both servers sent each walking bot the same number of distinct chunks over the whole run (680–690). Horizon's
"new chunks/s" inside the measured window is lower, so compare the totals, not that column.
### `explore` at full speed: unequal work
Bots fly outwards as fast as chunks arrive (`horizon-bots`' default, up to 22 blocks a second). Both servers now pull them back ("moved too quickly"), but Horizon sends
more new terrain, so this pass doesn't compare equal work. The server-machine load of Horizon's 1-bot run is
estimated from the time between the runs around it: its log was overwritten before it was read.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
"engine":"horizon-engine: the server build of #916 (horizon-client --no-default-features) at 519ec121, as install.sh --archive installed it",
"java":[
"openjdk 25.0.4.1 2026-08-18"
],
"settings":{
"servers":"horizon,vanilla",
"bots":"1,10,25,50",
"behaviour":"mixed",
"join_every":1.0,
"warmup":300,
"seconds":300,
"seed":12345,
"explore_speed":0.8,
"gamerule":"",
"heap":"4g",
"nice":19,
"bots_via":"ssh macmini",
"connect":"the ThinkPad's tailnet address",
"properties":{
"level-seed":"12345",
"view-distance":"10",
"simulation-distance":"10",
"online-mode":"false",
"white-list":"false",
"gamemode":"creative",
"force-gamemode":"true",
"spawn-protection":"0",
"max-players":"200",
"difficulty":"easy",
"sync-chunk-writes":"true"
}
},
"note":"server_bench.py was stopped after vanilla's 50-bot server crashed, before it wrote this file; the runs are the lines it printed, and the vanilla 50 entry is taken from that server's log.",
"runs":[
{
"server":"horizon",
"bots":1,
"behaviour":"mixed",
"start_seconds":19.73,
"mspt":{
"average":1.77,
"p50":1.34,
"p95":4.21,
"p99":13.04
},
"mspt_worst_p99":47.5,
"tps":20.0,
"rss_mb":{
"mean":686.54,
"max":686.7
},
"cpu_percent":27.92,
"chunks_per_second":9.94,
"unique_per_second":0.01,
"forgotten_per_second":9.9,
"waited_ticks_per_second":0.0,
"chunks_total":6092,
"chunks_per_bot":{
"walk":6092.0
},
"unique_chunks_per_bot":{
"walk":699.0
},
"resent":0,
"pulled_back":0,
"moved_too_quickly":0,
"moved_wrongly":0,
"bots_joined":1,
"bots_failed":[],
"tick_queries":58
},
{
"server":"vanilla",
"bots":1,
"behaviour":"mixed",
"start_seconds":20.45,
"mspt":{
"average":11.34,
"p50":10.6,
"p95":17.12,
"p99":33.22
},
"mspt_worst_p99":119.1,
"tps":20.0,
"rss_mb":{
"mean":3404.16,
"max":3416.3
},
"cpu_percent":35.93,
"chunks_per_second":9.98,
"unique_per_second":0.01,
"forgotten_per_second":9.93,
"waited_ticks_per_second":0.0,
"chunks_total":6084,
"chunks_per_bot":{
"walk":6084.0
},
"unique_chunks_per_bot":{
"walk":699.0
},
"resent":0,
"pulled_back":0,
"moved_too_quickly":0,
"moved_wrongly":0,
"bots_joined":1,
"bots_failed":[],
"tick_queries":59
},
{
"server":"horizon",
"bots":10,
"behaviour":"mixed",
"start_seconds":19.54,
"mspt":{
"average":4.74,
"p50":3.83,
"p95":10.48,
"p99":19.69
},
"mspt_worst_p99":120.1,
"tps":20.0,
"rss_mb":{
"mean":855.26,
"max":882.7
},
"cpu_percent":29.78,
"chunks_per_second":61.35,
"unique_per_second":1.79,
"forgotten_per_second":61.47,
"waited_ticks_per_second":0.0,
"chunks_total":40762,
"chunks_per_bot":{
"walk":6247.67,
"explore":1165.0,
"idle":473.0
},
"unique_chunks_per_bot":{
"walk":701.83,
"explore":1086.0,
"idle":473.0
},
"resent":0,
"pulled_back":4159,
"moved_too_quickly":0,
"moved_wrongly":0,
"bots_joined":10,
"bots_failed":[],
"tick_queries":58
},
{
"server":"vanilla",
"bots":10,
"behaviour":"mixed",
"start_seconds":19.95,
"mspt":{
"average":34.3,
"p50":32.2,
"p95":53.72,
"p99":89.08
},
"mspt_worst_p99":375.8,
"tps":20.0,
"rss_mb":{
"mean":3713.5,
"max":3754.9
},
"cpu_percent":128.07,
"chunks_per_second":61.66,
"unique_per_second":1.93,
"forgotten_per_second":61.71,
"waited_ticks_per_second":0.0,
"chunks_total":37568,
"chunks_per_bot":{
"walk":5729.0,
"explore":1124.0,
"idle":473.0
},
"unique_chunks_per_bot":{
"walk":701.17,
"explore":1055.0,
"idle":473.0
},
"resent":0,
"pulled_back":3976,
"moved_too_quickly":20,
"moved_wrongly":0,
"bots_joined":10,
"bots_failed":[],
"tick_queries":58
},
{
"server":"horizon",
"bots":25,
"behaviour":"mixed",
"start_seconds":9.61,
"mspt":{
"average":11.72,
"p50":9.88,
"p95":24.45,
"p99":43.79
},
"mspt_worst_p99":129.8,
"tps":20.0,
"rss_mb":{
"mean":947.35,
"max":1086.1
},
"cpu_percent":57.83,
"chunks_per_second":158.57,
"unique_per_second":5.78,
"forgotten_per_second":158.45,
"waited_ticks_per_second":0.0,
"chunks_total":108126,
"chunks_per_bot":{
"walk":6628.53,
"explore":1266.6,
"idle":473.0
},
"unique_chunks_per_bot":{
"walk":704.67,
"explore":1189.2,
"idle":473.0
},
"resent":0,
"pulled_back":3496,
"moved_too_quickly":0,
"moved_wrongly":0,
"bots_joined":25,
"bots_failed":[],
"tick_queries":58
},
{
"server":"vanilla",
"bots":25,
"behaviour":"mixed",
"start_seconds":37.78,
"mspt":{
"average":50.45,
"p50":46.39,
"p95":74.09,
"p99":191.21
},
"mspt_worst_p99":1388.1,
"tps":19.82,
"rss_mb":{
"mean":3866.18,
"max":4745.0
},
"cpu_percent":192.42,
"chunks_per_second":157.63,
"unique_per_second":8.25,
"forgotten_per_second":156.12,
"waited_ticks_per_second":0.0,
"chunks_total":88763,
"chunks_per_bot":{
"walk":5387.0,
"explore":1118.6,
"idle":473.0
},
"unique_chunks_per_bot":{
"walk":699.0,
"explore":1075.0,
"idle":473.0
},
"resent":2,
"pulled_back":3629,
"moved_too_quickly":296,
"moved_wrongly":0,
"bots_joined":25,
"bots_failed":[],
"tick_queries":58
},
{
"server":"horizon",
"bots":50,
"behaviour":"mixed",
"start_seconds":14.93,
"mspt":{
"average":23.82,
"p50":20.11,
"p95":50.98,
"p99":74.64
},
"mspt_worst_p99":195.9,
"tps":20.0,
"rss_mb":{
"mean":1031.41,
"max":1201.2
},
"cpu_percent":98.21,
"chunks_per_second":302.81,
"unique_per_second":10.96,
"forgotten_per_second":303.47,
"waited_ticks_per_second":0.0,
"chunks_total":212181,
"chunks_per_bot":{
"walk":6496.53,
"explore":1255.5,
"idle":473.0
},
"unique_chunks_per_bot":{
"walk":701.9,
"explore":1184.7,
"idle":473.0
},
"resent":0,
"pulled_back":7565,
"moved_too_quickly":0,
"moved_wrongly":0,
"bots_joined":50,
"bots_failed":[],
"tick_queries":58
},
{
"server":"vanilla",
"bots":50,
"behaviour":"mixed",
"start_seconds":11.16,
"error":"stopped by its watchdog 30 s after the last bot joined: \"A single server tick took 60.01 seconds (should be max 0.05)\" (Player.tick)"
| 100 | Vanilla | stopped by its watchdog | | | | | | |
Vanilla's watchdog stopped it at 50 bots (in `ClientboundLightUpdatePacketData`, copying light for a chunk
packet) and at 100 (in `Entity.move` from a move packet).
## Findings
- Horizon's RSS is 1.9 to 2.6 times smaller in every run, and it starts in 11 s against vanilla's 15 s.
Vanilla's heap is fixed at 4 GB (`-Xms4g`), so part of its RSS is that setting rather than need.
- With 1 bot Horizon's tick is 3.3 times shorter walking and 4.5 times shorter exploring at equal work;
at 10 bots it is still shorter in both.
- At equal exploring work Horizon's tick is about half of vanilla's at 10 and 25 bots.
- When the bots only fly around spawn (`walk`), vanilla's tick hardly grows with players (16 to 20 ms from
1 to 50 bots) while Horizon's grows by about 0.5 ms per bot, so vanilla is ahead from about 20 bots: 16.0
against 20.7 ms at 25 and 20.5 against 31.4 ms at 50. Both keep 20 TPS up to 50.
- With 100 bots vanilla's watchdog stops it in every pass, and Horizon keeps running at 9 to 11 TPS.
### Where Horizon's walk tick goes at 25 bots
Profiled on the ThinkPad with `perf` (the server thread, 20 s, self time):
| Share | What | Whose |
|---:|---|---|
| 18 % | `Streaming::update`: every tick it walks all loaded columns and proto chunks and tests each against every player's area (unloading, dropping protos, finishing and decoration candidates), so the cost grows with players times chunks | streaming / engine tick |
| 20 % | `PalettedContainer::get` and `World::block`, mostly from block entity ticks: on the Mac profile `tick_hoppers` looks up the block under every block entity in the loaded world each tick and compares names as strings, and spawners rebuild their display id | engine tick |
| 8 % | `light_data` when a chunk is sent: the light layers were built twice per chunk; a follow-up commit on #623 builds them once and keeps each section's packed layers | server (this work) |
| 7 % | encoding chunk sections and heightmaps for the chunks each bot newly sees | server |
| 4 % | `Hub::tick` (moves, tracking) | server |
| 2 % | the moved-wrongly collision check | server |
The first two grow with the number of players but aren't specific to the server: they are the
engine's tick and streaming, reported to perf-server-2. Before #623 and #649, encoding every chunk section
once per player and waking each connection's writer thread once per packet cost more than everything above
(at 50 walking bots the tick was 281 ms; see the earlier runs).
## Earlier runs (same machine and method)
-`run-1-before-chunk-cache.json`: before #623, with vanilla's locator bar on. Horizon's walk tick was
27.5, 40.7, 281 and 467 ms at 10, 25, 50 and 100 bots, and vanilla's 33.2 ms at 50 (the locator bar).
-`run-2-chunk-cache.json`: Horizon with #623 only: 13.2, 23.5, 31.4 and 84.4 ms walking at 10 to 100
bots.
## Files
-`horizon.json`, `vanilla.json`: the walk and explore passes (vanilla with `locator_bar=false`).
-`explore-equal-rate.json`: the explore pass at 0.8 blocks a second, both servers.
-`run-1-before-chunk-cache.json`, `run-2-chunk-cache.json`: the earlier runs.
To run it again (bots on another machine reached at `--connect`):
Loaded 100 of 1825 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.