🤖 Agent: six-face panorama check in horizon-prepare check.rs, rustfmt fix; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: reproduced #5352 with launcher 0.2.31 and Nightly d91cab6 in scratch homes; fixed launcher builds Zoom, Minimap, Recipes and X To Xray on d91cab6 (all ON in Options > Mods). pr_ready --quick green.
🤖 Agent: online_mode harness (finished from #5508): the services' key fetch stays out of the log. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: vaults' display item spins on remote clients (VaultBlockEntity.Client.tick); verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: a stack's own blocks_attacks component is read (Item.use, getItemBlockingWith); approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: enderman's carried block culls back faces (ITEM_CUTOUT); approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head (pr_ready --quick green for it). Parity rerun: the failed run was only online_mode (harness bug, #5627) / dedicated_server (flake #5560).
🤖 Agent: backup before restoring level.dat_old, other-series worlds refused on every open path; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head (pr_ready --quick green for it). Parity rerun: the failed run was only online_mode (harness bug, #5627) / dedicated_server (flake #5560).
🤖 Agent: reconnect decided by the disconnect's translation key; author's main merge leaves the diff unchanged; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head (pr_ready --quick green for it). Parity rerun: the failed run was only online_mode (harness bug, #5627) / dedicated_server (flake #5560).
🤖 Agent: 16 port-map rows say port; both review points fixed; data only. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: content mods declare thrown entity types (snowball template); rdbtCVS found no defects; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: server tag hook fragment fixed for the stamp tool (no game code); verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: lan_e2e punches vehicles fully charged so they break; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: horizon-mixins pin a2af5c0 (only horizon-mixins#15); approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: keybind-opened screens release the item use in the same tick (Gui.setScreen releaseAll), with the use-screen e2e scenario; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: the AppImage path test runs on Unix only (fixes the Windows launcher build); code-owner path, approved here. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: superflat structure sets (FlatLevelSource.createState) with every review point fixed (ring seed 0, sea level/min Y, null layers, no creature packs, generator min Y); verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: launcher strings through tr()/trf(), Turkish table completed; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: verified by Ludovic's AI agent (Claude). pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others: launcher fmt, clippy -D warnings, and launcher tests (one failure in backend::tests::prepared_versions_get_what_the_asset_store_adds_without_preparing_again was a prepare-lock race; it passes alone with the account tests). The owner gate runs as that signed-in account; no offline name. I fixed the review's doc nit.
🤖 Agent: boats in bubble columns as vanilla, countdown test added by rdbtCVS; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: opt-in shader-pack shadow cache (off by default), rdbtCVS's custom-image redraw fix read; approved; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: VRAM arena compaction, rdbtCVS's last fix (LOD stand-ins off the closing page) read; approved by rdbtCVS; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests). I set vanilla's forced-translucent colour (alpha 38).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests). I stopped the narrator on headless exit and moved idle's doc back.
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests). I added the server-only gate for the stuck push and one shared moveTowardsClosestSpace.
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests). I made the answer carry its level event, added API markers, and the stamp tool now keeps side.
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests).
🤖 Agent: verified by Ludovic's AI agent (Claude). I read the diff against vanilla 26.3. pr_ready --quick passed on main bfc2a2798 with this PR merged with 17 others from the same sweep (fmt, clippy -D warnings, horizon-client and horizon-client-render tests, Python tests). Ctrl+Q on the Survival Inventory tab uses the item's max stack size, like every other Horizon menu (none reads a max_stack_size component yet).
A rebuild stays on the Horizon its native build was made on, and mods pinning an
older commit (eight registry mods pin 142388e) set the build's revision too. When
a mod needs newer hook points than that revision has (recipes v2, xtoxray v7),
and the hook mods were tested on different commits, the build was refused with
"Horizon 142388e doesn't have the native hook points these mods use", even
though the installed Nightly has them all and no Update button was offered
(#5352). The build now moves to the revision of the release or Nightly it
starts from when that one has them; pinned mods then wait for Build anyway,
as for any version mismatch. The refusal names only the mods that don't fit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameRenderer.extract only renders the level when there is one, so vanilla
never draws the debug crosshair on the title screen. The F3 overlay stays
on when a world closes, and the crosshair was built without looking at
whether the frame showed a level, so its axes stayed over the panorama.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- native_dispense_item answers (remainder, event): 1000 click, 1001 fail
(OptionalDispenseItemBehavior without success), 0 nothing (NOOP), so a
mod can make a silent or failing behaviour.
- native_dispense_item and native_dispense_default carry the
API @API:dispenser_behaviour@ marker; the fragment lists dispenser.rs.
- stamp_native_hooks.py copies a hook's neoforge, side and cancellable keys
into native-hooks.json instead of dropping them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven on 8e5ab9ec6: pr_ready --quick green (fmt, clippy -D warnings, 1577 horizon-client tests, 510 Python tests); playhorizon A/B 3 alternated rounds 20 bots: streaming span 4.82 → 4.64 ms median (lower in every round), MSPT within noise. The 34 affected harnesses run in CI (run-parity).
🤖 Agent: native_explode: server-only wrapper over server_explode with the TNT/anchor drop-decay rules, guarded on dimension, loaded chunk and radius; verify and parity green on this head.
ExperienceOrb.tick gates the stuck push with !level().isClientSide(): a
server client (remote.rs) no longer nudges stuck orbs and draws from its own
random between server updates. Items and orbs share one
towards_closest_space port instead of two copies.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The namespaced-id check is Identifier's character rules, five lines; the
dependency pulled horizon-world (and horizon-core, postcard...) into
horizon-mods, the mod registry and the launcher builds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both the vanilla and the Horizon server fetch getPublicKeys in the background, at any moment of a
scenario, so ('unknown', '/publickeys') turned up in four checks (parity run 41243825430 on #5508).
It now stays out of the log, like a bare GET /.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: fixes#5572. ground never sends back and a dropped claim never feeds the floating kick. anticheat_e2e ground (allow-flight off) passes; vanilla 26.3 client soak 25 min never flagged; vanilla client at the live-flagged spots on the server's seed 42 min / 7 spots, 0 flags (18 min of it on this branch, ground acting). pr_ready --quick all green on main 74698584e.
player_model_shown() no longer blocks spectators, so their model
geometry generates in third person. The render path uses render_with
so the poses' visibility hides body parts, leaving only the head/hat.
body_color returns ~12.5% alpha for spectator players; OIT marks the
body already, so the ghost head looks ghostly.
Co-Authored-By: Hermes Agent <combo1> <[email protected]>
Clarify that only physical_keys is cleared directly in heal_hold_latches, leaving logical sneak/sprint keys to the toggle loop.
Co-Authored-By: Antigravity <[email protected]>
When ground acted, a false claim sent the player back to where it was:
into the air it claimed to stand in. A player there claims it again, is
sent back again (+0.5 VL twice a second at the same spot) and hovers until
vanilla's "floating too long" kick: in 11 minutes on the official server,
four players got it 20 times. Only the anticheat's own kick (logged, told
to the operators, counted toward the ban) should end a false claim.
ground's correction is now the dropped claim alone (vanilla's fall damage
follows), logged as "claim dropped"; it never sends the player back, and a
move whose claim was dropped never counts toward clientIsFloating.
setback = 0 still makes the check log only. The ground e2e runs with
allow-flight off and fails on a setback or a floating kick.
Fixes#5572
Co-Authored-By: Claude Opus 5.5 <[email protected]>
self.keys tracks the logical gameplay keys, and sync_toggle(0) uses KeyCode::ShiftLeft to represent active sneak regardless of the physical key binding. Calling self.keys.remove(&key) when physical Shift is up cleared active sneak every frame for players who rebound sneak away from Shift (e.g. to Left Control or C).
Fixes#5608
Co-Authored-By: Antigravity <[email protected]>
RenderPipelines.TEXT_POLYGON_OFFSET and TEXT_SEE_THROUGH (and the
TEXT_SNIPPET they build on) never call withCull, so
RenderPipeline.Builder.build keeps its default, cull.orElse(true). Horizon
drew both with cull_mode None, so a text display showed its mirrored
text from behind (#3833).
Culling alone would have hidden the readable side instead: the text
display quads were taken in the wrong corner order and wound clockwise
toward the reader. They now use Font's glyph order (top left, bottom
left, bottom right, top right), the one name tags already use, through
text_display::CORNERS. A test projects fixed and billboard displays
through the real view and projection matrices and checks that the side
the text reads from is the counter-clockwise one.
The improved-transparency text pipeline is culled the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: players can /msg, /tell and /w a player by their nickname (shown as ~name); server guests included. SMP proof and pr_ready --quick passed on this head (comment above).
A finished column at the edge of the generated area is lit against its
decorated proto neighbours, each copied into a column on the server
thread (light_view). The light engine reads only their blocks, yet the
copy also rebuilt every section's biomes (64 biome lookups and a
palette per section), the scheduled ticks, post-processing marks and
structure references. The view now copies the blocks alone; the final
column (finish) still gets everything.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.applyItemBlocking passes `damageBlocked >= damage` to
blockUsingItem with the damage Player.hurtServer already scaled by the
difficulty; Horizon compared against the raw damage, so on Easy a full
block counted as partial and blockedByItem's knockback pulled the
defender toward the mob. The hurt now records whether it was fully
blocked and the shield reads that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.setRemoved makes a removed vehicle's passengers stopRiding, and
LivingEntity.dismountVehicle puts them at the higher of their own and
the vehicle's heights, at the free spot nearest there (findFreePosition).
break_cushion only cleared the riding id, and a server-thread client kept
riding a cushion that was gone, so the rider stayed at the seat, 0.35
below the cushion, and dropped into the block under it.
Both the server (break_cushion) and the client (the cushion's removal)
now dismount the rider that way, with no get-up sound
(Cushion.removePassenger plays none once the cushion is removed).
Fixes#4571
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's first ServerEntity.sendDirtyEntityData for a player runs before
its first ServerPlayer.tick, whose updatePlayerAttributes (and doTick's
removeFrost) create the interaction range and movement speed instances.
Those go out on the next tick, after the first chunk batch. Horizon's guest
tick created them before sync_player_state sent them, so a vanilla client
got a default-valued update_attributes before the first chunk batch.
Fixes#5560
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A surface wgpu never configured has no error sink, so acquiring from it
panics instead of reporting Validation (#5353). recreate_surface now checks
configure through error scopes and a device-lost flag, and frames configure
such a surface again before acquiring, skipping until it takes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityHitboxDebugRenderer.emitGizmos draws every visible entity's box,
the Interaction too (its renderer is a NoopRenderer, the hitbox is not
the renderer's). Its box is EntityDimensions.scalable(width, height):
eyes at 0.85 of the height, not a LivingEntity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LoadingOverlay.extractRenderState blits textures/gui/title/mojangstudios.png
as two halves side by side over BRAND_BACKGROUND and draws its bordered bar
at 83.25% of the height; Horizon only filled the background. The logo comes
from the player's own prepared resources (the GUI atlas already packs it).
Fixes#5115
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server simulated its players' bodies with no world border (only the
window's own player got one, in player_border), so a server player's
re-simulated movement went through the border's walls. enter_guest now
gives the guest's body the level's border each turn, as vanilla's
Entity.collide collides every player with the level's WorldBorder.
Fixes#2743
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#5559 let the dialog registry carry data for a client that knows the
core pack; with this PR the server follows vanilla's packRegistry again,
so every registry, the dialogs too, goes without data to that client.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The classic path drew weather before translucent terrain with depth writes
on, so the water behind the rain failed the depth test and the rain columns
cut holes in it. Vanilla 26.3's RenderPipelines.WEATHER is
DepthStencilState(GREATER_THAN_OR_EQUAL, false), and executeClassicTransparency
draws translucent terrain, clouds, then weather, then the world border.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_remote_spawners ticked spawners and trial spawners on a remote client
but not vaults, so VaultBlockEntity.Client.tick (display item spin, idle
particles and sounds) never ran on the server-thread client.
Fixes#3108
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`horizon-launcher play --account WHO` plays this launch as a signed-in account (name or UUID)
without changing the one `account select` picked, so several games of one instance can play
as several accounts at the same time (#4952). The owner gate runs as for any launch, and
what the services renew (tokens, names, skins) is kept.
account::session_for_game_as runs the gate as that account on the loaded accounts and puts
their choice of player back before saving them (launch_as). A Java instance refuses the
flag, as it does --world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr_ready on the Mac mini (head 26748d7c1, base a4873dec4): fmt, clippy -D warnings, horizon-command tests 27/27 (new a_dedicated_server_keeps_seed_and_version_for_game_masters), Python 510/510, public tree ok; parity 41/44 affected harnesses pass incl. command_parse, command_exec, chat_screen, server_ops_feedback. The 2 failures predate this PR and don't involve commands: online_mode = this Mac's known IndexError (stray GET /, fails on main too), dedicated_server = config-phase minecraft:dialog registry data from #4698 (same root cause as #5559), the stage before any command. CI verify passed.
On a server where a mod set nicknames (native_set_nickname), a player
argument naming no player matches the player whose nickname it is (any
case, without the ~ chat shows, or with it quoted), so /msg, /tell and /w
reach a nicknamed player by the name chat shows. Real names win; no
nickname, no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ExperienceOrb.tick diverged from vanilla 26.3 in the way the dragon's
death XP shows: a landing orb kept its pre-collision speed through the
friction step and bounced back at 0.9 of it on every ground contact, so
the orbs rained onto the exit portal hopped high and for a long time —
the report reads them as floating in mid-air and spreading out.
The tick now follows the vanilla method: gravity 0.03 is skipped while
the orb's box is inside blocks (`!level.noCollision`), a stuck orb with
no player within 8 blocks gets Entity.moveTowardsClosestSpace (already
ported for items, now on the orb too, its own random drawing the
speed), the collided axes' velocities are zeroed the way
Entity.move's restituteMovementAfterCollisions does for a bounciness 0
orb (x and z by Mth.equal's 1.0E-5, y exactly), one friction of
getAirDrag() times the ground's friction — computed as a float, as
vanilla does — scales every axis including y, and the landing bounce
is `-fallSpeed * 0.4` gated on `fallSpeed < -getGravity()` instead of
a fixed -0.9. The friction anchor below the orb is getOnPos(0.999999)
as the item's already was, not the block one below.
tools/test_command_exec_parity.py gains an "experience orb fall"
group: an orb falling 45 blocks onto a stone platform, out of the 8
block follow radius, matches vanilla's Pos and Motion to the digit at
every checkpoint, the settled state included (969/969 with it).
Fixes#4781.
Co-Authored-By: JustKarkat's AI agent (ZCode) <[email protected]>
MineshaftStructure and StrongholdStructure call moveBelowSeaLevel with
ChunkGenerator.getMinY(), which is 0 for FlatLevelSource (the noise settings'
minimum Y otherwise), not the level's -64. The Classic Flat stronghold sat
64 blocks lower than vanilla's.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
CreativeModeInventoryScreen.slotClicked drops one item (the stack up to its
max size with Control) from a slot of the Survival Inventory tab whatever the
hand carries; the menu's THROW needs an empty hand, so with a carried stack Q
did nothing there. The screen's carrying branch now covers that tab as well as
an item tab's hotbar, and sets the slot as the menu path does (equipment
changes included).
lan_e2e creative_throw also checks Control+Q with an empty hand on an item
tab's hotbar, Q and Control+Q over a display slot, and Q, Control+Q and Q
while carrying on the Survival Inventory tab.
Part of #3684.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dropped item can be picked up again after 40 ticks; on a slow runner a
pickup between steps would change the counts the check compares.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a creative item tab, Q over a hotbar slot took the stack out through the
menu's THROW, but on a client of a server (single player's server thread, LAN,
dedicated) the drop only went to the server on the Survival Inventory tab, so
the slot was synced empty and nothing dropped. Send it on every creative tab,
as AbstractContainerMenu.doClick calls Player.handleCreativeModeItemDrop.
Also port CreativeModeInventoryScreen.slotClicked's own branch for a THROW on
an item tab's hotbar slot while a stack is carried: one item (the whole stack
with Control) drops from the slot and the carried stack stays.
lan_e2e: new check creative_throw, on the host and the guest.
Part of #3684.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
More review fixes on the flat generator:
- FixedBiomeSource.findBiomeHorizontal for the stronghold rings of a fixed biome source.
- The ring cache key carries the structure_overrides, not a constant.
- A structure_overrides list naming no existing set lets every set generate; an unknown
biome is plains.
- A flat generator that cannot be built leaves the world on its flat ground; the column
queries cover the whole level; far decoration is off for a flat world.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla reads the block at every drawn position of a ticking section; most
hold stone, dirt or air, and each read is a chain of cache misses (chunk map,
section, palette, words, block facts). Each ticking section now keeps a
4,096-bit mask of the blocks a random tick can do anything for (a randomly
ticking block, or lava), built at its first draw and dropped when the section
changes. The draws, their order and every tick that runs are the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: fixes#5505 (lan_bot 13/15 failing on main since #4698 always sends minecraft:dialog data). Test-only: the known-pack assertion allows data for the dialog registry. lan_bot 15/15 on this head with vanilla data; fmt ok.
Review fixes for the structures on a flat generator, from the 26.3 bytecode:
- createForFlat searches the stronghold rings with seed 0.
- FlatLevelSource's sea level is -63 and its generation context starts at Y 0 with depth 384.
- A null layer (not opaque to MOTION_BLOCKING) is skipped by getBaseHeight and
buildTerrain and reads as air in getBaseColumn; a FillLayerFeature fills it after the
structures (TOP_LAYER_MODIFICATION).
- spawnOriginalMobs is empty for a flat generator: no generation-time creature packs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Since #4698 the server sends minecraft:dialog's data even to a client
that knows the core pack (Horizon's client reads the dialogs from the
packet), so every bot join panicked on the configuration step's "no data
with the known pack" assertion and 13 of 15 lan_bot tests failed. The
assertion now expects data for the dialog registry and stays strict for
every other one.
Fixes#5505.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4698 made the minecraft:dialog registry always carry its entries' data,
on the idea that a client does not rebuild dialogs from its known pack.
Vanilla's does (RegistrySynchronization.packRegistry): an entry of a
pack the client knows goes without data, and the dedicated_server
harness saw Horizon send the dialog registry in full where vanilla sends
Optional.empty for each entry. Horizon's own client answers
SelectKnownPacks with no packs, so it keeps getting every entry in full;
a dialog a world's pack adds or replaces is marked from_pack and still
carries its data, so quick actions from a data pack reach every client.
Fixes#5525
Co-Authored-By: Claude Opus 5.5 <[email protected]>
break_vehicle's ten punches have run two ticks apart since they went
into one script (bd09f8b03). Player.attack scales a hit by its charge,
0.2 + s * s * 0.8, and a fist charges in 5 ticks, so each later punch did
0.4: 10 for the first, then 4 a hit against 2 of decay, 28 after ten of
the more than 40 a vehicle needs (vanilla would take 17). The punches
are now 5 ticks apart, fully charged, twelve of them: the vehicle breaks
on the 8th, and a tick of drift still nets at least 4 a punch.
Fixes#5526.
Written with Claude Opus 5.5 in OpenCode.
Commands(CommandSelection.DEDICATED) registers SeedCommand and
VersionCommand with checkPermissions (LEVEL_GAMEMASTERS) and leaves out
PublishCommand and UnpublishCommand (includeIntegrated). Horizon's
executor tree used the integrated rules on --dedicated, so any player
could read the seed of an SMP. The Commands packet already cut them; the
executor and /help now do too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
agent_control_e2e's use-screen scenario opens the inventory and the chat over
a golden apple in use (survival) and checks the use is released in that tick:
the stack stays at 64 and no effect appears, while eating with no screen still
finishes. With the call sites in game/remote.rs disabled it fails with the
apple at 63, so it covers the bin wiring the interaction-input parity harness
cannot see; the parity rows stay as oracle-side coverage.
Co-Authored-By: DeepSeek 4.1 Flash (DeepSeek Harness) <[email protected]>
A cross model (flower, fungus) carried by an enderman drew both of its
coplanar sides through the entity pipeline, which does not cull. The two
quads sat at the same depth and flickered against each other (#5421).
CarriedBlockLayer.submit draws the block through BlockModelRenderState,
which uses the item cutout sheet (RenderPipelines.ITEM_CUTOUT). That
pipeline keeps the default back-face culling, so only the side facing
the camera is drawn. Drop the quads whose face normal points away from
the camera after emitting the block model. CULL_BACK would also drop
them, but it turns off the normal lighting this model needs.
Refs #5421
A world whose Version.Series is not main went straight to the file fixes
and the upgrade in WorldRequest::prepare, which the dedicated server and
the agent launch both reach. Vanilla refuses it: the dedicated server's
Main.java returns before DataFixers.getFileFixer().fix when
summary.isCompatible() is false, and WorldOpenFlows.
openWorldCheckVersionCompatibility shows the incompatible AlertScreen
(LevelSummary.isCompatible, DataVersion.isCompatible).
The title screen already refused such a world. The open path now does
too, so an incompatible world is never upgraded and saved in place.
The Recover screen's Attempt to Restore moved the broken level.dat aside
and put level.dat_old in its place without copying the world first.
Vanilla RecoverWorldDataScreen.attemptRestore runs
EditWorldScreen.makeBackupAndShowToast before restoreLevelDataFromOld:
a zip of the whole world folder without session.lock. The restore runs
once that finishes, whether the backup worked or not.
The button now starts the same backup through the backup screen and
runs attempt_restore when it is done, with the backup toast.
native_has_analog_output and native_analog_output answer BlockBehaviour.hasAnalogOutputSignal and
getAnalogOutputSignal for a mod's block; Level.setBlock's comparator update follows the answer, and
native_update_output_neighbors is Level.updateNeighbourForOutputSignal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.move resolves Y first, where the body starts, then X and Z. On the
tick a player walks off an edge, or lands on one and slides off it, the
client is on the ground over air at the move's end. An instrumented
25-minute vanilla client soak on generated terrain matched all 117 of
its ground flags to this (104 walk-offs at the same height, 13 landings),
one per step down any slope or staircase: enough to push a player
running down a long one toward the kick. ground now also holds a claim
when the ground holds at the move's start X and Z at the new height. A
no-fall or jesus has nothing there either.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Opening the inventory (or chat, or advancements) while an item was in use
left the use running: the key event sets the screen, and the release sentence
of Minecraft.handleKeybinds never runs while a screen is open, so the serving
side kept counting and ate the food behind the GUI. Vanilla releases in the
same tick: the key opens the screen inside handleKeybinds, Gui.setScreen runs
KeyMapping.releaseAll (clearing key.use), and the same call then hits the
isUsingItem release. Add interaction_input::release_on_screen_open and call it
from the inventory/creative, chat and advancements key paths; pause, server
containers, mount inventories and dialogs keep vanilla's later release.
Fixes#4472
Co-Authored-By: DeepSeek 4.1 Flash (DeepSeek Harness) <[email protected]>
The new cart_slots was inserted between unpack_cart_loot and its doc
comment, so rustdoc merged both onto cart_slots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: new head (author merged main) keeps the hopper→jukebox fix and jukebox_taken (song stops when a hopper takes the disc); verify passes on aa68b6653, merges cleanly with current main (no overlap in jukebox/machines/spill/save)
🤖 Agent: rdbtCVS's merge of main (same resolution as mine, arm order only). clippy -D warnings clean; anticheat 20, server 88, lan:: 116 tests pass on the equivalent tree. lan_bot fails 13/15 on main itself (#5505), not this PR.
The official server's first two hours with the anticheat logging only
(67 players): every player who wasn't flying or walking on water peaked
at a ground level of 7, and that was 4 claims standing still in one
second (a block the server didn't have after a long teleport). The seven
who were (no-fall, fly, jesus: claims every tick while rising or falling
through the air, or walking on water; most also kicked by vanilla's
floating check) peaked at 113 to 3749.
ground now defaults to setback 10, alert 15, kick 30, ban_after 2, and a
claim standing still isn't judged (no fall to skip; hovering is
vanilla's floating kick). Replayed on that log, the others peak at 5
and the seven are kicked, then banned. timer and prediction still only
log: prediction flagged nearly everyone, and timer's slow decay lets lag
bursts add up over a session.
anticheat_e2e's ground check now runs at the defaults with cheat bots
that move, and wants the ban count in the kick line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging: two native helpers for server mods (nicknames in chat and the Tab list, the chat filter's verdict); the SMP mod's proof passed on this head with the fragment stamped. No change without a mod calling them.
Conflicts: game/lan.rs (the anticheat's HostEvent::Ban next to main's ChatFiltered) and horizon-server config.rs (main's [discord] and [chat_filter] blocks around the anticheat's ban_after wording).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added tr.json after this branch began, so it lacked the 12 keys the i18n
completeness test asks for. They hold the English source until translated, as the
other untranslated entries do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The nine lang tables conflicted where both sides added keys: main's table kept as
the base (its order and its translations), plus the keys only this branch has.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now clears a pack's custom images each frame before the shadow pass, which may write them; skipping that pass on a hit would leave them empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Measured after #5415: with the bots off GitHub's pool it ran 9-12 of its 20 jobs and nothing waited, while 19 jobs
waited for playhorizon's two checks runners (4 CPUs shared with parity), the oldest 18 minutes. The bots stay on
playhorizon-light; the checks runners take main's and same-repository pull requests' checks only when the
repository variable SELF_HOSTED_CI is `all`, for when GitHub's pool is full again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two documentation conflicts again, where main's management protocol lines grow at the same place as the RCON ones. Main's text is kept, and the RCON lines now sit away from them (before the management protocol's paragraph and its changelog line), so the next merge doesn't meet them.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The countdown of AbstractBoat.tickBubbleColumn moves to Boat::tick_bubble_countdown,
so a unit test checks it fires on the 60th tick over a column, with the column's
direction, and resets when the boat leaves.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's Item.use, getItemBlockingWith and applyItemBlocking read
DataComponents.BLOCKS_ATTACKS from the stack, so a sword given
`[blocks_attacks={}]` blocks like a shield. Horizon only read the item's
default component. BlocksAttacks::of_stack now takes the stack's own
component first (kept in StackData::other), then the item's, and the
local player's raising, use tick, blocking, off-hand intent check and the
remote client's use prediction go through it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QSY4AbxZbBtWqJLhFg
FlatLevelSource.createState: villages and strongholds (the classic flat
world's structure_overrides) and any set a world's settings allow generate on
the flat layers, with the fixed biome, and are found by /locate. The pipeline's
noise column is the layers (getBaseHeight, getBaseColumn), the terrain stage
fills them, and the structure steps run with no placed features.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A tile's previous mesh (kept while near chunks fade in) lives in the arena as well; without moving it, a closing page holding one stayed closed until the fade ended and compaction stalled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: management protocol 6e, ip_bans/* and operators/* applied by the game as vanilla's services: parity harness vs vanilla 26.3 passed (50 exchanges over TLS); clippy, fmt, horizon-server (88) and horizon-server-files (13) tests pass.
Two conflicts, both documentation: docs/api/server.md's changelog and docs/server.md section 8, where main added the management protocol's allowlist and bans. Main's text is kept and the RCON lines follow it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ip_bans/{add,remove,set,clear} and operators/{add,remove,set,clear}, as
vanilla 26.3's IpBanlistService and OperatorService make them, through the
game's management request like the allowlist and the bans: an address or an
online player's address, those online from it sent away; an operator's level
and bypass as asked, else op-permission-level and the bypass they had, the
commands online players may run updated. The parity harness changes all four
lists on both servers (50 exchanges).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: author's rebase on main, same content as reviewed; pr_ready --quick on main + #5242#5259#5275#5321#5354#5356#5357#5358 passes (clippy, 2389 client/render tests, Python 508)
🤖 Agent: author's rebase on main, same content as reviewed; pr_ready --quick on main + #5242#5259#5275#5321#5354#5356#5357#5358 passes (clippy, 2389 client/render tests, Python 508)
Called from native_explosion_detonate, native_explode took the outer
blast's source, cause, owner and hive/rail flags. Save and clear them
around server_explode, then restore them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QT61YPiPzVNysmVHsH
native_play_sound took minecraft: events only. Now an event of another
namespace (mymod:machine_hum) is sent to players by name, as vanilla sends a
sound that isn't in its registry (Holder.direct); they hear it when their
resources define it, as a content mod's sounds.json and .ogg files do.
minecraft: events must still be registered.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`/item replace`, `/item modify` and `execute if items` answered "The
target does not have slot container.0" for a chest or hopper minecart.
Vanilla's AbstractMinecartContainer.getSlot is ContainerEntity's
getChestVehicleSlot, which takes any index below the container's size
(27 for a chest minecart, 5 for a hopper minecart), and reading or
writing it unpacks the cart's loot table with no player first.
Slot::Cart gives commands those slots; cart_slots drops the loot table
the same way, so no player_generates_container_loot fires later.
The command execution corpus checks both carts against vanilla: 66/74 of
the items and loot group matched before, 74/74 now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main (mod_effects in the bounce test); pr_ready --quick on main + #5242#5259#5275#5321#5354#5356#5357#5358 passes (clippy, 2389 client/render tests, Python 508)
Both PRs correct rows of the same port-map tables; the conflicts were
neighbouring rows, each changed on one side only, so every row keeps the
side that changed it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: management protocol 6d, allowlist/* and bans/* applied by the game as vanilla's services: parity harness vs vanilla 26.3 passed (42 exchanges over TLS); pr_ready clippy/fmt/python pass; lan_bot's 13 minecraft:dialog failures come from main (#4698), not this PR.
Server owners asked for one identity in the channel (the webhook's name
and avatar, e.g. "HorizonSMP") instead of a per-player name and head on
each post. Drop the per-player username/avatar_url and the Line field
that only fed it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: queued for merge once verify passes (Ludovic's ask: keep operators' commands out of other players' chat on the official server; default stays vanilla).
🤖 Agent: merged main (the verify failure was main's title keyboard test, fixed in #5425). Wolf.getAmbientSound growls on isAngry (anger timer), checked in bytecode.
The fixture's server generates a 1024-bit RSA key in a debug build before it answers; on playhorizon's checks
runners (4 CPUs shared with parity, niced) that took longer than the 10 s the test waited for the session's event.
A passing run never waits for the timeouts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
available_parallelism is the process's share (its CPU affinity, a cgroup quota): pinned to 4 of 8 CPUs, the game
reported 4 threads and 8 cores, and this_machine_is_described failed on playhorizon's pinned CI runners. On Linux
the threads are the processors /proc/cpuinfo lists, where cores come from.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A systemd user service's umask is 0002 on Ubuntu, and the launcher's tests check the mode of the files they write.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GitHub's 20 hosted jobs at a time are shared by every pull request: on 2026-10-31 400+ runs queued and every job
waited 20-25 minutes for work of 0.3-4 minutes. Main's pushes and this repository's own pull requests now run
checks' verify and launcher on playhorizon (label playhorizon-ci, two runners); the bots that run main's own scripts
(pr labels, overlap note, cancel-on-close, claim command, parity-scope) on two small runners there
(playhorizon-light). A fork's pull request stays on ubuntu-latest, and the runners' job-started hook refuses one
whatever its workflow says. SELF_HOSTED_CI=off sends everything back to GitHub's runners.
tools/ci/checks-runners.sh installs the runners (systemd user services pinned to CPUs 4-7 in horizon-ci.slice,
shared cargo home, a build folder per runner outside the checkout); docs/ci-runners.md; tests/test_ci_runners.py pins
the fork rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main (the verify failure was main's title keyboard test, fixed in #5425). pack.mcmeta: first JSON value only, as Gson's JsonParser; BOM kept; test.
🤖 Agent: merged main (fragment stamped by #5390 → doc edits moved to API 50 section; main's dragon test updated to HurtSource); cargo test dragon + native pass
allowlist/{add,remove,set,clear} and bans/{add,remove,set,clear}, as vanilla
26.3's AllowlistService and BanlistService make them. The supervisor checks the
parameters and forwards each edit to the running game as a control request
(management <connection> <json>); the game finds the players as
MinecraftPlayerListService.getUser does (by id: the user cache, else the
session service; by name: the user cache, else the profile service, else an
offline player), changes its lists, logs RPC Connection #N lines and sends the
banned or unlisted players away. The reply is the list from its file.
horizon-server-files gains UserList::clear, UserCache::get_by_id and
profiles::find_by_id. The parity harness changes both servers' lists and
compares every answer (42 exchanges); neither server reaches a profile service
there, so names resolve to the same offline players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ocelot.mobInteract trust (1 in 3, events 40/41), DATA_TRUSTING synced and saved, removeWhenFarAway and the avoid goal; superset of #5423 (closed)
A mod item with its own fill level (a tank, a battery) had no way to draw
the bar vanilla's Item.isBarVisible/getBarWidth/getBarColor give tools and
bundles. native_item_bar fires where GuiGraphicsExtractor.itemDecorations
picks the bar, after the bundle override, with vanilla's choice in `bar`;
a mod may replace or hide it. Nothing changes when no mod hooks it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_set_nickname(uuid, Option<&str>) shows a name in place of a
player's in chat (Player.getDisplayName's text: their messages, /me,
/msg) and in the Tab list (PlayerInfo UPDATE_DISPLAY_NAME); the hover,
the suggested /tell and the insertion keep the real name, as do the
log, commands and the server's events. native_text_allowed(text) asks
the server's chat filter. API number from the fragment's stamp.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two conflicts, both documentation: docs/api/server.md's changelog and docs/server.md section 8, where main added the management protocol's TLS and reworded its list. Main's text is kept and the RCON lines follow it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: management protocol 6c, TLS on by default (PKCS12 keystore as vanilla, or a generated self-signed one): parity harness vs vanilla 26.3 passed over TLS (33 exchanges, TLSv1.3 both), horizon-server's 81 tests and clippy pass.
Upstream now sends custom_model_data itself; keep its codec and drop
ours, so this branch only adds custom_data to the LAN item sync.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The management server serves wss:// as vanilla's does: a PKCS12 keystore
(management-server-tls-keystore), its password from
MINECRAFT_MANAGEMENT_TLS_KEYSTORE_PASSWORD or the server property, rustls on
ring. Where vanilla stops without a keystore, Horizon generates a self-signed
one in .horizon/ and writes its path and password to [properties], so
management-server-enabled=true is enough; the start logs the certificate's
SHA-256 fingerprint. The WebSocket reads and writes through a Link (plain or
TLS, full duplex). The parity harness now runs both servers over TLS with one
keytool keystore.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
rdbtCVS's review of #5365: a kick only marks the player as leaving, so
packets still queued (a lagging client's ticks) were handled after it,
flagged again and counted a second strike, banning the player in the
session the first kick warned. flag_to now drops them and counts
nothing once the player is leaving. Without a ban list, anticheat_ban
falls back to the anticheat's kick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main again: save_guest follows main's every-player-file rule with this PR's playerdata API; main's new autosave loop (PlayerList.saveAll) calls playerdata::save without the old vehicle argument. Clippy clean, persist tests pass.
🤖 Agent: merged main; what remains is the Experiments switches committing on Done (Cancel drops them). Main's feature-aware load replaces the Re-Create fix. Client builds.
🤖 Agent: merged main (most of this stack landed via #5069); what remains is pack item tags reading main's tags/<ns>:<registry>/<path> keys (tag_kind) plus two loot tests; namespace tests pass.
🤖 Agent: merged main up the stack; #4448's atomic vector rewrite carries main's new Preprocessed.warnings; #4790 handles Enter on its shader buttons in main's new keyboard focus path (#4792: shift+Enter resets, as shift-click). Each layer builds; horizon-shaders tests 76 pass.
🤖 Agent: merged main up the stack; #4448's atomic vector rewrite carries main's new Preprocessed.warnings; #4790 handles Enter on its shader buttons in main's new keyboard focus path (#4792: shift+Enter resets, as shift-click). Each layer builds; horizon-shaders tests 76 pass.
🤖 Agent: merged main up the stack; #4448's atomic vector rewrite carries main's new Preprocessed.warnings; #4790 handles Enter on its shader buttons in main's new keyboard focus path (#4792: shift+Enter resets, as shift-click). Each layer builds; horizon-shaders tests 76 pass.
🤖 Agent: Server bench against Pumpkin (black box, GPL-3.0 source never read): the runner, its tests, the doc and the 2026-10-31 report. CI green at this head, pr_ready pass (498 Python tests), 24 matrix runs on playhorizon with none failed.
native_world_drawn gives a mod the frame once the world is drawn: the device,
queue and encoder, the colour target, the world's depth, the matrices with and
without TAA's jitter, the entity quads with their runs, the atlas and the quad
indices. native_render_needs lets mods ask for the entity runs and the world's
depth without a shader pack; several can ask. Both live in the render crate
(gfx/native_render.rs), and the catalog test accepts hook points there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
server.toml [server] admin_feedback_to_ops (default true, vanilla's
broadcastToAdmins) is written as horizon-admin-feedback-to-ops; false
keeps the [name: ...] lines to the console's log. Players who aren't
operators never get them either way (g.permission > 0, as vanilla's
isOp, checked by the server_ops_feedback harness).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lines name players only by the event's `name`; a death message the game wrote
with a nickname gets the real name after it (smp-1 is adding /nick).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#5327 landed `Mannequin.updateDescription` (the default label, hidden, or the
server's component under the name tag) and the two synched-data slots beside
the profile, so both rows stop saying missing.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`LivingEntity.entityEventForEquipmentBreak`'s note still said a player's
hands, the saddle and a guest's tool were not broadcast. Main's #5392 covers
the player half (both hands, armour, shield, a guest's included) and #5317
the mobs' half, so the row points at the slot-to-event table itself now and
says where both halves live.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`ShulkerBoxBlockEntity.moveCollidedEntities` was marked unsure in the
checklist; it is `game/piston_push.rs::Game::push_shulker_entities`, whose doc
names the method and moves every body the sweeping lid box meets along the
facing by the box's size + 0.01. That was the last row of #3516 without a
port.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`Arrow.updateColor` is `Projectile::arrow_effect_color` (its doc names the
method on the way past), `Cat.CatTemptGoal.canScare` is `CatTempt::can_continue`,
`Entity.applyImplicitComponent`/`applyImplicitComponentIfPresent` are the
component pass `entity_nbt::apply_item_components`, and
`LivingEntity.swapHandItems` is the entity-event-55 arm in
`remote.rs::Game::handle_packet`.
`Fox.canHoldItem` and `Fox.pickUpItem`, checked in the same pass, have no
implementation in the code at all (the generic `equip` gate lists the fox as
one whose pickup is elsewhere, and nothing is), so they stay out.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`TrialSpawner.FullConfig.overrideEntityData` is
`mobs/trial_spawner.rs::TrialSpawner::override_entity_data`, reached from the
dispenser's egg by `Game::use_spawn_egg_on_spawner`; and a blast breaking an
item frame (`ItemFrame.shouldDamageDropItem`) is
`game/explosion.rs::Game::explosion_hurt_entities`'s `explosion_breaks_frames`.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`ShelfBlock.useItemOn` was pointing at the client-side reaction
(`game/remote.rs::remote_use_item_on`) and its two swaps were still `missing`
for want of a citation; all three are `game/shelf.rs::Game::use_shelf`,
whose doc names the method and whose two arms are the single swap and the
powered chain's hotbar swap (`block.shelf.multi_swap`), with the block
entity update through `Containers::container_changed`.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Eighty-eight rows of #3512, #3514, #3516 and #3517 are ticked in their
checklist while `tools/port_map/` still calls them `missing`: the pull
request that fixed each one ticked the box and did not touch the map. This
corrects the 63 that cite the vanilla method in port code — 36 through a doc
comment on the port itself, 27 through a mention whose note says so and gives
its file and line. Every `where` names a function that exists.
The 23 ticked rows with no citation at all are left alone and listed in the
pull request: a tick is not evidence enough to flip a verdict.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Sheep.shear spawns each wool item with spawnAtLocation(stack, 1.0F): an
ItemEntity a block above the feet whose constructor sets its motion to
(rand*0.2-0.1, 0.2, rand*0.2-0.1), with the default pickup delay, and then
adds its own spread ((f-f)*0.1, f*0.05, (f-f)*0.1) on top. Horizon gave the
drops only that spread and spawned them at the body's centre, so the wool
left the sheep with at most 0.05 blocks/tick upwards and fell almost
straight down.
MushroomCow.shear builds each ItemEntity by hand at getY(1.0), so its
mushrooms get the constructor's motion and no pickup delay; the loop now
tells the two apart instead of applying the sheep's spread to both.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Ocelot.mobInteract: raw fish from within 3 blocks while the tempt goal runs is
used up; one time in three DATA_TRUSTING is set (entity event 41, hearts),
otherwise smoke (event 40). A trusting ocelot neither runs from players nor is
scared while tempted, is saved as Trusting, and no longer despawns when far.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Keeps main's arm (the anticheat drops a cheating attack, a move behind a
kept attack waits) and this branch's Hub::tick drain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
python3 tools/stamp_native_hooks.py on main 1e16d07: 11 fragments in
first-parent merge order (47 entity_join_level, 48 block_items,
49 native-sidebar, ... 57 chunks_players).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The one conflict was docs/api/server.md's changelog: main's longer management protocol line and the RCON line are both kept.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
#4638's keyboard test expects Enter on Create New World to open the create
screen at once, but 072cc6127 (landed beside it) shows vanilla's
"Preparing for world creation" screen first, as the other title tests
expect. The test now finishes the preparing step before checking.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A key bound to key.pickItem was turned into the middle button only while
playing; behind a screen it was dropped, so pick block on X did nothing in
the inventory or a creative tab. AbstractContainerScreen.keyPressed clicks
the hovered slot with CLONE (button 0) for it, as the middle button does.
Not on the creative search tab, whose box takes the key, nor behind a dialog.
Fixes#5378
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server never put setSprinting's movement speed modifier on a guest's
body: START_SPRINTING only set a flag. Its movement speed then synced
without the modifier, and a vanilla client, which takes the modifiers as
sent (handleUpdateAttributes), walked while it sprinted: after a speed or
slowness change mid-sprint, until it stopped sprinting. handlePlayerCommand
now sets the modifier as setSprinting does.
The prediction's replay copied all the server's attributes on any
attribute or effect packet, the movement speed included, so it lost the
sprint modifier the client kept and played a sprint as a walk (a false
flag of a vanilla client after spreadplayers). An attribute packet now
changes the replay's sprint modifier only when it carried the movement
speed, as the client's does; effect packets copy only the effects. A
replay that starts again sprints as the client's first tick does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4787 moved the players' packets into Hub::drain_packets, which the game
calls between poll and tick_level; Hub::tick, the whole-tick helper the
lan_bot server uses, kept only poll and tick_level, so 8 of 14 tests saw
none of their packets handled. The bots of
guests_see_each_other_and_the_world_entities now send ClientTickEnd after
each move, as a client does: a second move in one client tick is kicked.
Fixes#5364.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A check that kicks now counts each player's kicks, across sessions and
restarts (anticheat-strikes.json). The kick that reaches the check's
`ban_after` bans instead: banned-players.json through the normal ban list
(source "Horizon anticheat", reason "Horizon anticheat: <check>"), the
player sent away with vanilla's ban message. combat bans at its second
kick; timer, ground and prediction keep ban_after = 0 until trusted.
Every kick and ban is logged with its evidence and told to the console
and online operators, so an op can /pardon a wrong one.
Also merges hub.rs's two S::Attack arms: the anticheat's shadowed #5123's
(unreachable pattern), so an attack never stopped the moves queued behind
it. Both apply now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now hands title actions a &dyn EventLoopCtl; #4638's keyboard test
predates the preparing read Create New World goes through.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dedicated server now refuses players' text that holds a hate slur before it
relays it: chat, /msg /tell /w, /me, /say, /teammsg, sign text, anvil names and
book titles. The sender is told in private, logs/chat-filter.log keeps the
original text for operators, and horizon-server's event stream gets a
chat_filtered event instead of chat (so the Discord bridge never forwards it).
The 2nd hit within 10 minutes mutes for 10 minutes, the 3rd bans for 24 hours.
Mutes persist in chat-filter-mutes.json; ops lift them with /unmute <player>
(console: unmute). server.toml's [chat_filter] sets it, on by default.
The new horizon-chat-filter crate holds the settings, the precompiled matcher
for the list (data/slurs.txt, or the owner's words_file) and the escalation.
Text is normalised first: case, accents, look-alike letters, invisible
characters, leetspeak, separators and repeated letters, with care for Niger,
snigger, knight, suspicious and the like.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
enable-rcon, rcon.port and rcon.password were read from server.properties but
nothing listened. The supervisor now serves vanilla's RCON (RconThread and
RconClient in 26.3): it listens on server-ip (every address when empty) and
rcon.port, logs in with the password, and runs each command through the same
console request as `horizon-server cmd`, so it answers while the game restarts.
Settings follow RconThread.create (no password or a port outside 1 to 65535
leaves it off with vanilla's warning; a port that can't be bound is a warning).
The packets follow RconClient: login 3, command 2, replies in packets of 4096
characters, -1 for a refused login, "Unknown request <hex>", and "Error
executing: <command> (<reason>)". Differences: packets are read from the
stream, the password is compared in constant time, a connection that hasn't
logged in within 30 seconds is closed, and at most 16 are open.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: the player half of onEquippedItemBroken (hands 47/48, armour, shield, guest breaks broadcast); complements #5317 (mobs) without touching creatures.rs
This reverts commit 3d5138cfd: #5392 broadcasts the player's equipment
breaks (both hands, armour, shield, a guest's included) through
onEquippedItemBroken, so with both the guest's main-hand break would be
sent twice. This PR stays the mobs' half.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
control.rs: main's control_command_inner takes the loop handle
(&dyn EventLoopCtl) like the rest of the game's methods.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read the transport slice: custom component IDs behind a OnceLock table, checked in the fallback arm only (vanilla ids unchanged), unnegotiated stacks dropped; two integration tests
JukeboxBlockEntity.removeItem goes through setTheItem(EMPTY), which stops
the song and clears has_record; the hopper extraction path now does the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`JukeboxBlockEntity` holds its disc in a `ContainerSingleItem`, so a hopper
or a dropper reaches it like any other container and a disc that lands
starts the song. Here the disc was a bare `Jukebox.record` that
`BlockEntity.slots` did not expose, so `HopperBlockEntity.getContainerAt`
found no container and nothing could be inserted (the
`JukeboxBlockEntity.setTheItem` and `.canPlaceItem` rows of #3516).
`Jukebox.record` becomes `items: [Option<ItemStack>; 1]` with a `record()`
accessor, which `slots`/`slots_mut`/`contents` now hand out; `Containers`
learns `Kind::Jukebox` and its `canPlaceItem` (a playable disc, into the one
empty slot); and a successful transfer calls the new
`Game::jukebox_inserted`, which starts the song the way the player's own use
does (block state, level event, neighbours and comparator).
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
native_summon spawns any entity from its compound as /summon does;
native_move_entity, native_set_display and native_remove_entity move,
pose (transformation eased over ticks) and remove the entities Horizon
keeps without simulating them: displays, markers, interactions. An
item_display with a mod's own item model is a model that moves.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QSxLfzUbudHhcvZtgy
#5226 added the proxy, game arguments, release headers and sponsor strings
to the other languages; ja, pt-br and tr (added beside it) never got them,
so the launcher's every_language_has_every_string_the_code_asks_for test
fails on main and on every PR's launcher check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack with text after the closing brace of its pack.mcmeta was left out of the pack list, because serde_json refuses trailing input. Only the first JSON value is read now, so such a pack shows up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
🤖 Agent: merged main into the reviewed head (clippy had failed on main's old unreachable S::Attack arm, fixed in 825a6bbcf). es_es.json completed (198/198 keys, placeholders kept, JSON checked); asset only, no code.
🤖 Agent: merged main into the reviewed head (clippy had failed on main's old unreachable S::Attack arm, fixed in 825a6bbcf). read the diff against vanilla's SulfurCube.hurtServer: a burning arrow or trident lights an explosive cube's long fuse at the hit (tnt_explodes), after the hurt; pending sounds become flags so primed and hit both play; unit tests.
🤖 Agent: merged #4445's new head (86e0d83db, main merged) up the stack; #4790 also merges current main (lib.rs: shader_options beside main's split_profiles); top of stack builds clean (client lib+bin).
🤖 Agent: merged #4445's new head (86e0d83db, main merged) up the stack; #4790 also merges current main (lib.rs: shader_options beside main's split_profiles); top of stack builds clean (client lib+bin).
`LivingEntity.onEquippedItemBroken` broadcasts the equipment-break event for
the hand, so every client plays the break and its crumbs. A guest's tool
breaks on the host (`Game::damage_held` acting for the guest), and until now
nothing told the other clients: it was heard only by the window that
happened to be simulating. The break now goes through
`Game::lan_entity_event` with `equipment_break_event(MainHand)` (47).
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`LivingEntity.entityEventForEquipmentBreak` has a client-side inverse here
already (`item_break::equipment_break_slot`); the wear added for #5317 spelled
the event ids out itself. `item_break::equipment_break_event` is that method
by name, `hurt_mob_armor` asks it for the slot's id, and a test pins the two
as inverses of each other over every slot.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`LivingEntity.doHurtEquipment` covers all the equipment a living entity
wears, not only a horse's body armour: `Game::hurt_mob_armor` now takes
`max(1, damage / 4)` off the helmet, chestplate, leggings and boots too,
Unbreaking and the fire-resistant spare applied, and a piece that reaches
its maximum empties its slot and broadcasts the equipment-break event for
it (`LivingEntity.entityEventForEquipmentBreak`: 49 to 52 for the armour,
65 for the body).
`tools/port_map`: `LivingEntity.entityEventForEquipmentBreak` is `inline`
now, with what it still misses written in its note.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`Horse.hurtArmor` (`LivingEntity.doHurtEquipment`) was not modelled at all:
`Mob::damage_with` had no notion of worn durability, so a horse's armour
kept its full durability however often the horse was hit, and the piece
never broke (the row of #3512).
`damage_with` now reports the hit's dealt amount for a mob wearing body
armour (`MobEvent::ArmorHurt`, which carries the damage kind), and the host
applies it in `game/creatures.rs::Game::hurt_mob_armor` the way the player's
own `hurt_armor` does: `max(1, damage / 4)` with Unbreaking applied, no
wear for a piece that does not wear when hurt or for a fire hit on a
fire-resistant one, and at the item's maximum the slot empties and the
equipment-break event (65, the body slot) goes out. A wolf's armour keeps
its own rule (`Wolf.actuallyHurt`, `hurt_wolf_armor`).
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`Frog.FrogPathNavigation` is an `AmphibiousPathNavigation` whose
`prefersShallowSwimming` override costs the water nodes deeper than ten
blocks under sea level, but `frog.rs::register` never changed the
navigation kind, so a frog pathed as a plain walker: it would not cross
water at all and no frog ever reached a shallow bank (the
`Frog.FrogPathNavigation.createPathFinder` row of #3512).
`NavKind::Amphibious` now carries the preference (`Amphibious {
prefers_shallow }`), which the path finder already had a field for and
always read as false, and a frog sets it. The axolotl, drowned and turtle
keep the plain form.
`tools/port_map/game-012.tsv`: both `Frog.FrogPathNavigation` rows are
`port` now.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`FlatLevelGeneratorSettings.updateLayers` and `.adjustGenerationSettings`,
`LevelChunk.postProcessGeneration` and `LevelSummary.backupStatus` are all
in the code (`flatgen.rs`, `game/postprocess.rs`, `save.rs`); the checklist
rows describing them as unported are stale.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
The checklists of #3512/#3514 assumed these were unported; reading both
sides says otherwise:
- `FallingBlockEntity.getTypeName` is no port at all: the anvil death text
is its damage type's message id, which takes no block-name argument.
- `Ocelot.OcelotAvoidEntityGoal.canUse` is `tame.rs::OcelotAvoid::can_use`,
registered last for a wild ocelot.
- `Axolotl.Variant.getSpawnVariant` loses a stale gap: spawn never draws the
blue variant in vanilla either, and the 1-in-1200 draw is on breeding.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Block and item displays, a saved text display, the spear's stab count, a
firework rocket's entity hit and the arrow and stinger counts a player
carries are all in the code already.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
The second half of the blocks, redstone and fluids list: the jigsaw and
structure block editors, a spawn egg on a trial spawner, the campfire,
chorus flower and decorated pot a projectile lights or breaks, the wind
burst that opens doors, trapdoors and levers, the flower pot an empty hand
empties, the light block's picked level, and the It Spreads trigger.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
The same read of the checklists against `main` for the animals, entities and
world lists (#3601). Eighteen rows move from `missing` to `port`, and five
already-`port` or `inline` rows lose a gap note that has since been closed:
schooling-fish followers path to their leader, a copper golem opens doors on
its path, a mule never mates, a tamed wolf's kill is its owner's, and loot
tables apply set_components.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Re-read the open rows of the blocks, redstone and fluids list against `main`
(#3601). Nineteen of them are in the code already, so their verdict-map rows
stop saying `missing` (or lose a gap note that has since been closed):
- the whole structure block screen and its behaviour: the mode buttons, size
detection between corners, save, load, place-with-settings and the redstone
trigger are `game/structure_blocks.rs`, and the outline box and render mode
are `horizon-client-render/src/structure_block.rs`;
- `TurtleEggBlock.destroyEgg` is the shared `BlockLandingRegistry::destroy_egg`
of the fall and step rolls;
- `LeverBlock.useWithoutItem`'s dust puff is `Game::lever_puff` on the client
that clicked;
- `LightBlock.getCloneItemStack`, `MagmaBlock.stepOn`,
`RedStoneOreBlock.spawnParticles`, `BeehiveBlock.playerWillDestroy` and
`TripWireBlock.playerWillDestroy` all have their port.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Main grew `MobWorld.universal_anger` (`NeutralMob.isAngryAtAllPlayers`)
after this branch was written, so the test's MobWorld literal stopped
building. Restores the one-line fix the maintainer's agent had pushed here
before a rebase of mine overwrote it.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`Mob::do_move` rebuilt `Entity.restituteMovementAfterCollisions` by hand and
only ever used the entity's own `bounciness` attribute, so the block's
restitution never reached a mob: a slime block or a bed under a falling mob
stopped it dead where vanilla sends it back up (`Entity.getBlockBounciness`,
the row of #3514).
The port of that method now drives mobs too:
`movement_restitution::restitute_movement`, which the player and dropped
items already use and which `tools/test_restitution_parity.py` compares with
vanilla, is bound to a mob's own fields by `MobRestitution`. That also drops
the hardcoded honey check: which blocks suppress a bounce is the movement
data's `suppresses_bounce` tag, as for every other entity, and the
gravity comparison uses the living entity's effective gravity (0.01 while a
Slow Falling mob descends) instead of the bare attribute.
Mobs still sneak never, so nothing suppresses their own bounce.
`tools/port_map/game-005.tsv`: the `Entity.getBlockBounciness` row loses its
"mobs ignore it" gap.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Contributors' agents that rebase and force-push over maintainer fixups
erase them (#5188, #5203). Ask for merges of main instead, and say what
happens if the fixes get erased anyway.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: re-read after the author's update: only crates/horizon-mod-registry/Cargo.lock regenerated against main (adds zip and its deps), so mod-registry's --locked clippy passes; the launcher strings reached main elsewhere.
Main added Japanese and Turkish catalogs and new strings since this PR;
the i18n test asks every language for every string any catalog holds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4787 moved the players' packets into Hub::drain_packets, which the game
calls between poll and tick_level; Hub::tick, the whole-tick helper the
lan_bot server uses, kept only poll and tick_level, so 8 of 14 tests saw
none of their packets handled. The bots of
guests_see_each_other_and_the_world_entities now send ClientTickEnd after
each move, as a client does: a second move in one client tick is kicked.
hub.rs's packet loop had two S::Attack arms (the anticheat's, then
#5123's, which keeps the moves queued behind an attack for the next tick):
the second was unreachable, which fails clippy -D warnings on main and
a_move_behind_an_attack_and_its_swing_waits_for_the_next_tick. One arm
does both now (the same hunk as #5365).
Fixes#5364.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read the diff: a software (DeviceType::Cpu) adapter skips the GPU occlusion pass by default, HORIZON_GPU_CULL still forces it either way; hardware unchanged; measured +5 % FPS on llvmpipe; unit test. Merged main, dropping the branch's duplicate #5251 fixes.
🤖 Agent: read the diff: screens read Shift/Ctrl from the OS modifier word (InputWithModifiers.hasShiftDown), the word reconciles stale physical keys and held latches on ModifiersChanged (toggles, pad holds and rebinds kept), Shift-drag distributes a carried stack, pad Y quick-moves, Ctrl+Q reads the same word, horizon-ctl key/release keep it in sync; new lan_e2e checks. Merged main: #5208's per-frame heal reads the same word, so the two agree; cargo check -p horizon-client --lib --bins --tests clean.
Main's new mirror_group shares the terrain bind group layout, which this
branch extends with the emission-thresholds texture.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.onEquippedItemBroken broadcasts entityEventForEquipmentBreak
(47 hand, 48 off hand, 49 to 52 armour) and each client plays breakItem: the
stack's break sound and the crumbs. The acting guest's breaks go out with
their entity id when their turn ends; the shield's sound is its break_sound
component, not a sound the server plays.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The same mixed bots on playhorizon (release builds, CPUs 0-3, three rounds):
Horizon's tick is half Pumpkin's at 100 bots, equal at 60, and 1.8 times
longer at 20 (where it generates 63 % more new chunks a second). Idle
memory, start and median join are behind. The report, how to read it, and
the commands to run it again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/server_bench.py --servers horizon,pumpkin --pumpkin <binary>: Pumpkin
(a binary built by the user, run as a black box) gets its own default
configuration with the benchmark's settings, listening on 127.0.0.1 with its
Bedrock and LAN listeners, query, plugins and telemetry off. --repeat runs
every server and bot count again, alternated; a run with 0 bots measures the
idle server; each run records the bots' join time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A build recorded each mod's digest over its mixins/<id>/ folder (after repinning
its copy), but the Mods page digests the whole .hnmod, horizon-native.json
included. They never matched: every registry native mod showed Needs rebuild
while Build said no build was needed. The build now records the digest the page
reads, taken before repin.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read the diff: lan_e2e/agent_control_e2e run on Windows (junction fallback on WinError 1314, no vscreen), horizon-ctl launch clears HANDLE_FLAG_INHERIT on its std handles before spawning the game (cfg(windows) only); Linux/macOS unchanged; Python tests. Proof on Windows 11 in the body.
🤖 Agent: read the diff against vanilla's ChatScreen.normalizeChatMessage: String.trim, normalizeSpace (Java whitespace) and the 256 UTF-16 unit cut before sending and before the history; unit tests.
🤖 Agent: survival: a replaced armour piece that doesn't fit is thrown from the eyes like Player.drop(stack, false), as the other Inventory.add-else-drop paths; 2 lines after #4231 took the rest; rdbtCVS approved. Parity rerun (lunge_motion failure unrelated).
Main merged #5148, which appends its stock-layout test to the same place at
the end of `game::tests` where this PR appends its two, so the hunk
conflicted. Both sides are whole `#[test]` functions: kept both, each with
its own closing brace, in main's order after this PR's.
`cargo test -p horizon-client --bins` runs all three green
(`a_mod_screen_on_a_machine_menu_takes_the_stock_layout_out` from #5148, and
this PR's `an_interact_packet_clicks_the_entity_it_names_not_the_nearest` and
`an_interact_packet_cannot_name_a_mob_the_pick_would_refuse`).
Mining a block above while jumping into reach: the attack stays held,
the aim is lost on the way down and found again on the next jump, and
the block never finishes breaking. Three new cases (pick, no tool,
stone by hand) over the miss-flicker pattern the report describes.
All 4,199 cases match the pinned 26.3 client, so the client-side
breaking logic (sameDestroyTarget resets, stopDestroyBlock on a miss)
is vanilla; the reported divergence must live elsewhere — the eye-ray
(reach/eye height) or the server's delayed destroy.
The scan over live and queued particles only runs once there are at least
ParticleLimit.SPORE_BLOSSOM of them in all, so a spore blossom costs nothing
extra in the common case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main into the reviewed head (clippy had failed on main's old unreachable S::Attack arm, fixed in 825a6bbcf). read the diff against vanilla's Level.sendBlockUpdated / ChunkHolder.broadcastChanges: native_block_entity_sync puts a loaded position in the block log, so the hub sends the block and its update tag once a tick; lan_bot test; tested in game by arismoko. Merged main, API number moved to a hook fragment.
🤖 Agent: merged main into the reviewed head (clippy had failed on main's old unreachable S::Attack arm, fixed in 825a6bbcf). read the diff: native_menu_button_v2 fires right after native_menu_button with the clicking player's cursor stack (clickMenuButton(Player, int)); a change is written back and sent with SetCursorItem; API number from a hook fragment. Marked ready.
🤖 Agent: merged main into the reviewed head (clippy had failed on main's old unreachable S::Attack arm, fixed in 825a6bbcf). read the diff against vanilla: PiglinBrute.wantsToPickUp (golden axe only), StartAdmiringItemIfSeen(119) with StopAdmiringIfItemTooFarAway(9) and StopAdmiringIfTiredOfTryingToReachItem(200, 200), wasHurtBy clearing the memory and ADMIRING_DISABLED for a player's blow; unit tests.
🤖 Agent: merged main and resolved debug_keys.rs: main's F3 Density line (#4389) is now debug_entry_lines' chunk_generation_stats arm, unchanged; cargo check -p horizon-client --bins clean. Reviewed earlier: vanilla's DebugEntry* lines and six debug renderers, profile datafixes; nothing extra per frame with F3 closed.
🤖 Agent: secure chat 3/3 after #4376: the client signs chat and /msg on enforce-secure-profile servers (fixes the official server's 'missing profile public key'); Chat Delay flush marks messages processed, command signing waits for the server's tree, reconfiguration ack as vanilla; tested by 4404c with a real account; main merged by the author.
The rebase dropped 24349e636's fixes to native_move_fluids (inverted put-back
check, a tank marked with no fluid in it, other tanks written away; and a
room limit so a cauldron's thirds never need to go back) and #5188's newer
head. This puts them back on #5188's head, with the move in fluids_move.json
as in the rebase.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server mod's helper for Level.explode with no source entity: blocks
break as NativeBlast::Keep, Block or Tnt say (the drop decay rules),
entities are hurt and pushed, fire is optional, and players near hear
and see it. Refused on a client, for another dimension, an unloaded
chunk, or a radius outside 0 < r <= 32.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QSxLfzUbudHhcvZtgy
boot_paint.rs imports Win32_Graphics_Gdi and Win32_UI_WindowsAndMessaging,
which stopped resolving after #4441's Cargo.lock pass; main does not
compile. Also the test's language map resolves the keyed refusal.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
A tank dropped for an invalid capacity shifted the later tanks' numbers, so
the face and fill lists reached the wrong tanks (rdbtCVS's review). Also fixes
write_tanks' doc: an empty tank is written as minecraft:empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShulkerBullet.hurtServer: the hurt sound, fifteen crit particles and destroy.
The bullet is pickable and attackable (no pick radius, unlike
AbstractHurtingProjectile), so the crosshair finds it, a guest's attack
packet reaches it, and the blow plays Player.attack's sounds for a target
that is not a mob.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- test_table_distinguishes_missing_scene_load_from_zero: #4407's later
commit makes table() refuse reports without matching conditions, so its
fixtures now carry the same full conditions as the contract tests.
- test_the_repos_list_is_the_nightlys: the list names Qwerner88880 since
f35210d2d; 10c71a65d updated test_nightly_set but not this test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Shader API [reflection] option and mirror-eye facing culling after #4379; only native shader packs see it; rdbtCVS's points fixed; merged up the stack, shader-api tests and a horizon-client check pass.
🤖 Agent: read the diff against vanilla's DownloadedPackSource (DOWNLOADED_PACK_SELECTION): active server/world packs show in the Resource Packs list as required, fixed at the top, and their server/ ids stay out of options.txt; unit test.
🤖 Agent: read the diff against vanilla's FallingBlockRenderer.shouldRender: a falling block whose state equals its cell's is not drawn; one block read per falling block per frame; unit test.
🤖 Agent: re-read the author's rebase onto main (only context in lan.rs changed). read the diff against vanilla: model light probed at the pose's eye height (crouch render offset undone, Joansitoh's review addressed), setPosToBed and xRot 0 on entering and while sleeping, stopSleeping's findStandUpPosition + teleport on wake for the local player and guests. CI failures were main's Audio build break (#5251).
🤖 Agent: LockCode.canUnlock (spectator or the main-hand item) and createMenu's refusal (action bar + block.chest.locked, both halves of a double chest) match vanilla; the lock stays in the loader's source tag, so saves keep it; approved by rdbtCVS's agent
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff against vanilla: a UUID target resolves through the level's entities (ServerLevel.getEntity), not only mobs; the scoreboard holder lookup shares it; execute on owner keeps only living owners (EntityReference.getLivingEntity). Corpus cases added to command_exec; CI parity runs them. CI failures were main's Audio build break (#5251).
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff against the 26.3 tag sulfur_cube_with_block_immune_to: the eight missing damage types added to the cube's immune list (fireworks left to #4997); unit tests. CI failures were main's Audio build break (#5251).
🤖 Agent: read the diff: flat draws now exist wherever the device supports them, but frames without a pack keep per-section draws off Metal/Intel Gen11 (min_sections usize::MAX), so only a pack's terrain and shadow pass draw flat; gives packs gl_Normal and mc_Entity on NVIDIA/AMD, measured black-box. CI green, parity runs on the head merged into main.
🤖 Agent: read the diff: with a shaders/ pack (oldLighting unset) terrain and fluid vertices drop the baked face shade, measured black-box against Iris with a probe pack (no source read); without a pack, lighting::cardinal as before. CI green, parity runs on the head merged into main.
🤖 Agent: read the diff: while a mod screen is shown on a hopper or 3x3 menu, the stock grid no longer answers first in slot_position, so the mod's slots and inventory_y apply; unchanged without a mod screen. Tested in game by arismoko; CI green, parity runs on the head merged into main.
The picture has no box: the glyphs' shadows keep them readable on Discord's
light and dark themes (Anthony's feedback). A chat_filtered event (the server's
chat filter) is never forwarded nor accepted in discord.events. [discord] is a
real section of init's template (webhook "" = off), so the settings reference
lists its keys; docs/api/server-reference.md regenerated. Alt text and fallback
text are cut after defusing, within Discord's limits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
default no longer needs the template's properties
Both points arismoko raised by reading at e7f4440f0.
1. The NBT tables and the collision overrides carried one row per *template*
state, not per combination, so `facts.json` and the tables disagreed about
how many states a block has.
`nbt` (`states` lists, `cached_shapes` arrays) now emits a row per
combination in the order `states` numbers them in — a template state's
combinations are one run, and `Rename::id` maps the template state to the
first of it — with the declared properties on each row (the `properties`
compound `BlockState::to_nbt` writes; without them `BlockTable::check`
fails with "state identities differ" even once the lengths agree).
`collision_shapes` wrote `first + i`, so only the first combination of each
template state got a shape and the rest kept the table's -1. It now covers
every combination of every template state, all pointing at that state's one
shape: the geometry is the template's, the properties are the block's.
`structure-blocks.json` declared `states` as the template's states alone;
it now counts them times the fan-out, so the declared total and the lists
agree.
2. A `default_state` key the block declares itself was looked up on the
template anyway, so `{"template": "minecraft:stone",
"state_properties": {"lit": ...}, "default_state": {"lit": "true"}}` was
refused with "template default has no state properties" — stone has one
state and no properties at all. `combinations` already checks a declared
key's value, and only a key the template has narrows which of its states is
the default, so the lookup now happens only when such a key is present.
Three tests, each checked red before green: with the old `nbt` the table gets
4 rows where `facts.json` has 6; with the old `collision_shapes` the block's
last state maps to -1; with the old count `structure-blocks.json` says 4 where
its own lists hold 6; and with the old `default_state` guard the property-less
template is refused.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Main's heightmap tiles built owned columns; with this PR's zero-copy
meshing the tile and edge columns are references, so the heightmap
columns are built first and borrowed. Spans replaces Vec in the
heightmap and its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
the_openssl_script_signs_what_the_launcher_checks runs
tools/ci/sign_release.sh with `bash`. On Windows, Command looks in
System32 before PATH, so with WSL installed it starts WSL's bash.exe,
which can't open the script's Windows path and the test fails. The
script only ever runs on the ubuntu-latest `sign` jobs of launcher.yml
and nightly-build.yml, so skip it on Windows the way the test already
skips a machine without OpenSSL 3. Linux and macOS run it as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_move_fluids moves up to its max between two blocks, one fluid,
native_move_items' shape: what the target won't take goes back into the
source, past the fill sides' leave. The source's tanks just gave the fluid
up, so they have the room for exactly what left them, and a cauldron
levels back up by the thirds it gave, which a drain hands out whole.
Co-Authored-By: ZCode AI agent (GLM-5.3)
Merges origin/main (#5167's join hook beside this leave hook). Main's blast
on experience orbs, a mob picking up a whole stack and native_set_item_entity
emptying a stack now tell native_entity_leave_level too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Properties.store ends every line with BufferedWriter.newLine, which writes
System.lineSeparator(): "\r\n" on Windows (checked with JDK 25 there).
store() already does the same through line_separator(), but
stores_sorted_and_escaped and defaults_match_vanilla_init_settings
expected "\n" and failed on Windows. The first now expects the platform
separator exactly; the second reads the stored lines as "\n". Nothing
changes on Linux or macOS, where the separator is "\n".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review points from rdbtCVS on #4948: a failed read now clears the flag
before failed() so the error screen stays up with Create as its back
target; type_text is blocked during the read like the key paths; the
docs of finish_preparing, title_action and world_worldgen_overrides sit
on their functions again, and set_data_packs' orphan doc is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read the workflow: verify runs its python, fmt/clippy and test steps in one job again (one runner per run instead of four, the runner queue was 300 deep); the required check is still verify, the only one branch protection requires; no game code.
🤖 Agent: read the diff against vanilla: the dispenser's spawn egg now runs the same EntityType.updateCustomEntityTag step (apply_spawn_egg_entity_data, with its guards) a player's egg does.
naga's constant evaluator lacks mix, smoothstep, reflect, the pack/unpack functions and a few
more built-ins, so a const initialised with one fails the program ("Not implemented as constant
expression: Mix built-in function"; 66 times in Bliss). GL drivers fold them.
A global const that calls one now joins the globals our main initialises (as const ... ? ... : ...
already does), and a local const that calls one loses its const and is evaluated in place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
boot_paint.rs imports Win32::Foundation, Win32::Graphics::Gdi and
Win32::UI::WindowsAndMessaging, none of which were in the dependency's
feature list, so every Windows build of main fails at horizon-threads
(E0433, the items are configured out). Linux CI never compiles this
file, so it stays green while Windows builds, including the launcher's,
are broken. The same file passed the font count mutably where the API
takes it by const - Linux clippy cannot see that either.
(The hub.rs duplicate S::Attack arm this pull request first also fixed
has since landed on main by itself.)
Co-Authored-By: ZCode AI agent (GLM-5.3)
🤖 Agent: Ludovic's decision (2026-10-31), one line in .github/trusted-reviewers.txt; merged main (verify had failed on main's Audio break, fixed by #5251).
🤖 Agent: read the diff: queued native mod messages are flushed after native_server_stopping while the host and players are still there, on the integrated and dedicated servers; in-game proof on both paths in the body.
🤖 Agent: its owner merged main into the reviewed head (95cdad8b3, unchanged). read the diff: helpers only (per-player sidebar objective, Tab header/footer, tick stats, sleep-count opt-out for AFK players, yaw/pitch on NativeServerPlayer), nothing per tick; the hook fragment is stamped after merge. pr_ready green and the SMP proof on a dedicated server with two clients, per the body.
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff against vanilla: SulfurCube.knockback sets needsSync before setDeltaMovement; unit test.
- appimage_path: only when this binary runs from inside $APPDIR, so a launcher started by another
AppImage's program never overwrites that program's file.
- AppRun no longer exports LD_LIBRARY_PATH (the game and every other child inherited it):
patchelf sets the binary's and libraries' run paths; CI installs patchelf.
- Agent (MCP) configs and desktop shortcuts start update_target(), not the copy in the mount.
- The AppImage is built from $BIN like the archives.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff: x / 0.0 and sqrt of a negative literal go through a private hz_zero global naga can't fold (GL's run-time inf/NaN), texelFetch1D/2D/3D map to texelFetch; unit test. Shader-pack translator only.
🤖 Agent: merged main again (the earlier CI failure was main's broken Audio initializer, fixed by f80c0360d); clippy -D warnings and the horizon-bots tests pass on the merge. Approved by rdbtCVS; bots-only, no game code.
A guest's completion request was answered over the host process's registries, whose player list on a
dedicated server is the one local player "Player". The server now answers with its real online players and,
as vanilla's providers do (OpCommand, DeOpCommands, PardonCommand, PardonIpCommand, WhitelistCommand), from
the operators, banned names and addresses, and the whitelist.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Mannequin.DATA_IMMOVABLE and DATA_DESCRIPTION (after Avatar's two and the
profile), updateDescription (default entity.minecraft.mannequin.label, none
for hide_description, or the saved component) and isEffectiveAi: an immovable
mannequin never travels. The client draws the description under the name
(ClientMannequin.belowNameDisplay) within 10 blocks.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A new [discord] table in server.toml (webhook, events, enabled) starts a forwarder
on the supervisor's typed event stream: chat, joins, leaves, deaths (and kicks).
Lines of each 2 s are drawn as one PNG of the in-game chat box (half-black box,
§ colours, bold, 1-pixel shadows, wrapped at the chat's 320 px) with Pixelify
Sans (SIL OFL, already in horizon-theme), never Mojang's font. At most one post
every 2 s, longer when Discord's rate-limit headers or a 429 say so; a burst
past 20 lines is counted, not drawn; plain text when drawing fails.
No pings: every post sets allowed_mentions {"parse": []}, and every @ (and <@,
<# ...) gets a zero-width joiner, in the alt text and the fallback text too.
One player's lines carry their head (mc-heads.net by UUID) as the avatar;
their address is never sent. Only the webhook's host is accepted (Discord),
the API and panel show it as "(set)", and the API can't change [discord].
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read the diff: remaining launcher strings (proxy, game arguments, releases, patch notes, sponsor, mod verdicts) go through tr() with entries in the seven catalogs; no behaviour change.
🤖 Agent: read the diff against vanilla's ShearsItem.useOn: shears on a growing plant head below age 25 set getMaxAgeState with block.growing_plant.crop, a block_change game event and one wear; bodies and max-age heads untouched. New use_item_on cases; CI parity runs them.
🤖 Agent: a resumed dig keeps the first START's clock (ServerPlayerGameMode destroyPos/destroyProgressStart, 0.7 split) on the server and the client; dig_clock, dig_stop and instant_digging LAN checks pass per the author; elythepreciouscat's remote_mining fix included; rdbtCVS approved; verify, clippy, tests green. Parity runs on the merged head.
🤖 Agent: read the diff against vanilla's PlayerHeadItem.getName: a profile with a name titles the head block.minecraft.player_head.named in tooltips and command feedback; custom name still wins; unit test.
🤖 Agent: FSR 3.1 frame interpolation (MIT, AMD FidelityFX 1.1.4) ported to WGSL, still unused (skip-release); rdbtCVS's div_ceil point fixed in 58b2c98d3; naga-validated, GPU tests by the author; verify green.
🤖 Agent: verified: Zip::open now keeps the file and reads only the central directory onward (as Java's ZipFile), entries read at their offsets with no shared cursor; parsing unchanged; tests cover lazy reads, long directories and broken END counts. Thanks!
🤖 Agent: read the diff: the shaders/ preprocessor's #if evaluates float literals (integer / and % kept without one), and an unreadable condition is a warning reading false instead of skipping the program; unit tests. CI green, parity runs on the head merged into main.
🤖 Agent: read the diff: sky, celestial and cloud lightmap coords, the sky normal, cloud face normals (w 0, the game's own clouds unchanged) and blockEntityId -1 by default, as a shaders/ pack reads them in Iris (observed with a probe pack, no source read). CI green, parity runs on the head merged into main.
🤖 Agent: None now samples terrain as vanilla (linear clamped sampler, sampleNearest unless RGSS; terrain.fsh), and distant tiles repeat their sprite per block, which also fixes RGSS/Anisotropic stripes on main since #4843 (checked in game by the author). rdbtCVS's change request was addressed (dismissed with his re-review). verify green.
🤖 Agent: GL backend: indirect execution without compute shaders, batched indirect draws with the same strides/offsets; rdbtCVS checked fmt/clippy on third_party/wgpu-hal by hand and approved. CI green.
🤖 Agent: HDR tiers for lightning, beacons, crystals, fireworks, orbs; marks follow their draws on classic and OIT paths; checked in game by the author; rdbtCVS approved. CI green, parity pending.
🤖 Agent: read the diff: video modes are listed once per monitor instead of at every slider step (about 28 ms each on Windows); with_option keeps the cached list. Unit test; approved by rdbtCVS; CI green, parity runs on the head merged into main.
The title shown while resources load takes Tab, the arrows and Enter the
same way and hands its focus to the game's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Screen.keyPressed's focus navigation on the targets the controller's focus
mode already reaches: Tab and Shift+Tab in tab order (reading order),
arrows with the ported nextFocusPathInDirection step, Enter, Space and
keypad Enter press the focused widget. Sliders follow canChangeValue,
text fields keep their caret keys, a list is one Tab stop and the world
list selects its focused row. The focused widget is the narration's
focused one, the keyboard's own pointer events don't reach the narrator,
and the real mouse takes the pointer back where it left it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The control socket's key and type commands skip the window's events, so
the open screen never heard them (afterKeyboardAction): the chat's typed
message wasn't narrated in scripted runs. They now go through the same
narrate_key as the window's keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shared button drawer takes the narration a button records
(ui::button_as), so the game rule switches say their name, value,
description and default as BooleanRuleEntry's custom narration does,
and the command block's output, mode, condition and trigger buttons say
"<name>: <value>" and the value a click moves to. The rules list is a
ContainerObjectSelectionList: its rows are narrated with their place,
the category headers by their label, and the rule tooltips the list
draws are no hint of the rows' widgets. The command box and the search
box are numbered where vanilla adds them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options pages narrate their list as a ContainerObjectSelectionList: the
row under the mouse or focused, its position, the widget in it and its
header rows; cycle buttons the value a click moves to, key binds
narrator.controls.*, tooltips as hints. The language and pack lists are
object lists, the pack screen and its confirm screen name themselves, and
dialogs narrate their title, warning button, checkboxes, cycle buttons,
edit boxes and button tooltips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/narrator.md: the statuses and hotkey, the voices per platform (flite
needs libflite on Linux, as vanilla), where the code lives, how a screen
records its narration, silent scripted runs with horizon-ctl narration,
and the known differences from vanilla. Linked from the docs index.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An EditBox that can't be edited isn't active, so World Options' port box
is not counted while LAN is off (updatePortControlsState). The recipe
book's button moves when the book opens; a stable key keeps the focus
the click gave it, so it is narrated as vanilla's focused ImageButton.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The book is one narratable, hovered whenever it is visible, that narrates
the recipe button, search box, filter or tab findNarratableWidget picks
one level in, with no position of its own. The book button before it is
an ImageButton without a message.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
World Options' switches are CycleButtons (their value and the one a
click moves to), the difficulty lock a LockIconButton ("Difficulty lock
button. Locked/Unlocked"), the category headers FocusableTextWidgets,
and the port box is numbered after the LAN switch where it is added.
The statistics' general and mob lists are ObjectSelectionLists naming
the hovered row, the item list a ContainerObjectSelectionList of sort
buttons and items, as StatsScreen's are.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world list, the server list with its LAN header and games, and the
superflat layer and preset lists narrate their rows as their entries'
getNarration; the create-world tabs as a TabNavigationBar, its cycle
buttons and switches with the value a click moves to, and the edit boxes
by their messages. The title screen's and pause menu's icon buttons say
their own messages and tooltips in the order the screens add them, and
the multiplayer screens name themselves (joinForNarration for the confirm
and disconnected ones). A world search narrates the filtered list at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Screen.shouldRunNarration is GameNarrator.isActive: with the narrator off
its narration still runs and is marked said, so the click that turns it on
in the options says only what it changed 200 ms later. Widgets can now
record text widgets, cycle buttons with their own narration, and lists'
rows by name; a shared drawer's record can be named or moved after the
fact, and the drawers hear which widget the narrator has focused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
property_setters_refuse_no_change searches native_hooks.rs, read with
include_str!, for function heads written with "\n". Git for Windows'
default core.autocrlf checks .rs files out with CRLF, so the head is
never found and the test fails there. Read the source with its line ends
normalized, as the launcher's mojang_downloads_only_for_an_owner already
does (9cdfc5bd4). The test still finds the same code on an LF checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stats, advancements, world options (its lock popup, restrictions and game
rules), Play with Friends, server links (with the warning button and its
tooltip), the link and custom screen confirms, the pack prompt and the
code of conduct (title and text, its checkbox) name themselves as their
vanilla screens' getNarrationMessage does; the game mode switcher has no
title. The stats tabs narrate as a TabNavigationBar. The command block
screen has no title and takes the suggestions' usage while they show;
its boxes and the rules' and port's boxes carry their EditBox messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The chat screen says only its title, how to send (with the restricted
note) and the message typed, as ChatScreen.updateNarrationState does, and
its link prompt joins the question and the link as ConfirmLinkScreen.
The level loading screens (the startup menu's, single player's start, a
world opening, a server's terrain) have no title and no navigation and
say the server's progress every 2 s; the connect screen has no title;
the saving screen says its title alone. The win screen narrates its
narrator components, the poem's lines and the credits' names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A container screen narrates its menu's title (the creative inventory's
is empty, a merchant's is its name without the level); slots are never
narrated. The book screens say their title and page indicator (the view
also the page's text), their page box or title box and page buttons; the
sign editor its title and Done. The creative, recipe book and anvil
boxes carry their EditBox messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window's input reaches the screen's narration before the screen takes
it, as MouseHandler and KeyboardHandler call afterMouseMove,
afterMouseAction and afterKeyboardAction; a controller's focus step
counts as an arrow key. The startup menu narrates its screens and hands
its narration to the game with the title, and Ctrl+B schedules the open
screen's narration (updateNarratorStatus). The title screen's modes, each
options page, the pause menu and the death screen name themselves as
vanilla's screens do, joinForNarration for the confirm and alert ones.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ScreenNarrator reads each drawn frame's screen and narratables: a new
screen narrates at once (init's triggerImmediateNarration), then stays
quiet for 2 s, or until the next key after a keyboard; mouse moves,
clicks and keys schedule the narration that Gui.update runs when due.
updateNarrationState picks the widget as findNarratableWidget does, with
its position and usage, lists' rows and container lists' row widgets, and
a click focuses what it hits as mouseClicked does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drawers say which screen they draw (begin_screen: its id, title and
narration overrides) and the buttons, sliders and edit boxes they draw
record what they narrate, as Screen.updateNarrationState reads its
NarratableEntries. Nothing is recorded or built unless the narrator asks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NSSpeechSynthesizer posts to the run loop of the thread that speaks; the
narrator's speech thread now runs its own each time it idles, and idles
every 50 ms however many messages arrive in between.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The narrator speaks through AppKit's NSSpeechSynthesizer with vanilla's own
queue: a message waits while another is said, an interrupt stops the speech
but keeps the queue, and clear() empties it. The speech thread runs no run
loop for the delegate, so idle() polls isSpeaking and runs vanilla's
didFinishSpeaking when the speech has ended.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A read-only command beside stats and messages: the Narrator option's
status, whether text-to-speech is there, and the last 32 messages the
narrator was asked to say, so scripts can check narration without
listening (HORIZON_NO_AUDIO gives an active, silent narrator).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The narrator loads libflite and libflite_cmu_us_kal16 at run time as JNA
does, splits each message at punctuation and plays the pieces on its own
"Narrator #0" thread; clear() bumps the batch so queued pieces are skipped
while the one playing finishes, as vanilla's executor does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChatListener: player and disguised chat are said once they show, Chat
Delay included, with the chat type's narration decoration ("Steve says
hi", from the server's chat_type registry, chat.type.text.narrate when
it lacks the type); system chat as saySystemChatQueued when the Chat
option lets it through; the action bar's system messages as
saySystemQueued. Single player and LAN take the same path: the window
gets them from its server thread as packets.
Also: the jukebox's Now Playing and the creative hotbar's saved message
(saySystemNow), the player list's names as it comes up, ConnectScreen's
"Joining the world" every 2 s and LevelLoadingScreen's "Ready to play"
as it closes, a LAN world found in the server list's view, the selected
command suggestion (once per entry, at once when Tab asked), debug chat
(showDebugChat) and the world options' publish messages. Leaving a
server and a reconfiguration clear the narrator (Minecraft.disconnect,
clearClientLevel). Only the window's Game narrates, never a server
thread's in the same process.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The comment claimed a knot is a block under the crosshair rather than an
entity id. On a server it is both: `knot_ids` (`game/lan_entities.rs`)
gives every fence knot a `LeashFenceKnotEntity` id, so a guest's Interact
naming one arrives with an id that `interact_knot` cannot use, because it
starts from the block pick.
The comment now says that instead of stating a false reason. No behaviour
changes: knot ids were not resolved before this PR either.
rdbtCVS's open nit at 44c54ba8b. Also rewords the same claim in the body.
The previous merge point (`06485bfef`) is ten commits behind main, and it
still has what `825a6bbcf` fixed: two `S::Attack` arms in `Hub::process_inbox`,
the second unreachable, which is what makes `clippy -D warnings` fail on this
branch as it did on main. Merging current main takes that fix.
No conflicts, and no file this PR changes is resolved by hand here.
BlockAttachedEntity.ignoreExplosion: the explosion's direct source entity
in water makes the entity ignore it, before the mobGriefing rule. Primed TNT,
TNT minecarts, creepers, sulfur cubes, withers, fireballs, wither skulls and
end crystals report whether they are in water. An armour stand is not a
BlockAttachedEntity and keeps its own rule.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A press or repeat of B with the edit modifier (hasControlDownWithQuirk)
moves the narrator to its next status when the narrator is there, the
Narrator Hotkey option is on and no focused edit box takes the keys (chat,
command block, anvil name, world options' boxes, a dialog's text input,
the options' searches and the title screens' fields). The change is
announced with its toast and saved, on the startup menu, the title, the
menus and in game, and the key still reaches the screen.
on_narrator_status_changed marks where vanilla calls the screen's
updateNarratorStatus, for screen narration later.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-narrator ports vanilla's narrator core:
- GameNarrator's status rules (NarratorStatus, queued vs. interrupting
messages, the status announcement and its toast keys)
- com.mojang.text2speech's NarratorWindows on SAPI's SpVoice, on a speech
thread of its own so the game never waits on it
- ScreenNarrationCollector and Screen's narration timing (delays, the
suppression after init, findNarratableWidget)
HORIZON_NO_AUDIO gives an active but silent narrator for scripted runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
updateNarratorStatus's SystemToast (NARRATOR_TOGGLE) goes up with
"Narrator Enabled" and the status, "Narrator Disabled", or "Not Available"
when there is no narrator. As SystemToast.addOrUpdate, a toast still up
takes the new text and its display time starts again instead of a second
one stacking. It takes a free ToastManager slot beside the advancement,
tutorial and pack toasts and draws over every screen: the title (its own
toast keeping the first slot), the options, loading and in game, F1 hiding
it. The startup menu has its own until it hands the title to the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Narrator button cycles Off, Narrates All, Narrates Chat and Narrates
System, reads "Not Available" and is inactive while the narrator isn't
(Options.narrator's caption, OptionsSubScreen.addContents), and a change
runs GameNarrator.updateNarratorStatus: the speech is cleared and
"Narrator : <status>" said. Chat Settings shows it where vanilla's
ChatOptionsScreen does. The Narrator Hotkey switch is a plain toggle with
its macOS tooltip there. options-set narrator announces a new status too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client opens horizon-narrator where Minecraft's constructor makes its
GameNarrator (never for a server), runs checkStatus (logged: Horizon has no
native message box), and destroys it when the window's event loop ends.
Whenever options load or change, on the title's startup menu and in game,
the narrator takes the narrator option's status and the voice volume
(getFinalSoundSourceVolume(VOICE): master times voice).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content block's states were exactly its template's, copied row for row, and
`default_state` could only overlay values on properties the template already
had. So a pipe could not have a connection per side, and a machine could not
have `facing` and `lit`.
A block file may now declare `state_properties` — a boolean, or an enum of
values — and the block's states become its template's states times their
combinations, appended after the template's run so a block's states stay one
contiguous run in registry order.
The combination order puts every property at its declared default first, and
`Rename` carries how many states a block gives each of its template's, so a
template state still moves to exactly one state: the block's default. The
`facts.json` defaults and `render-definitions.json` mappings are unchanged.
Refused: a name its template already has, a bad name or value, a
`default_state` value the property does not have, and more combinations than
a world holds (2^20) — the last before the product is built.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
players, players/kick, server/{status,save,stop,system_message} and the four
lists' getters (allowlist, bans, ip_bans, operators), as vanilla's
PlayerService, ServerStateService and the list services answer them: players as
PlayerDtos, bans with their source, reason and ISO-8601 end, operators with
their level, the status's empty player list left out as vanilla's codec leaves
out its default. Kicks, saves and system messages go through the game's
console (tellraw, title actionbar for an overlay). server/status answers while
the game restarts, started false.
The parity harness now gives both servers the same lists and checks these too
(32 exchanges), the notifications vanilla sends meanwhile set aside.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
management-server-* in server.properties ([properties] in server.toml) turns on
vanilla 26.3's management protocol, served by the supervisor so it answers while
the game restarts. As net.minecraft.server.jsonrpc does: the secret as a bearer
token or in the minecraft-v1 subprotocol from an allowed origin (401 bodies
included), JSON-RPC errors and batches as Connection handles them, by-name and
positional parameters as IncomingRpcMethod checks them, a generated secret
written back, and a wrong secret or TLS setting stopping the start.
This first part: the WebSocket (std only, websocket.rs) and rpc.discover
(vanilla's OpenRPC document, management_schema.rs). The other methods answer an
internal error saying they aren't in Horizon yet; they come next.
tools/test_management_protocol_parity.py sends the vanilla server and Horizon's
the same messages and handshakes and compares the answers, and rpc.discover with
the data generator's json-rpc-api-schema.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's DebugEntryChunkGeneration line (#4389) was drawn inline in
draw_debug_overlay_under, which this branch replaced by debug_entry_lines:
it is now that function's chunk_generation_stats arm, unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved the launcher's completion checks into horizon_prepare::missing_extras, which
--check and COMPLETIONS share, so lacks_panorama and its test live there now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added Japanese and Portuguese (Brazil) since this PR's keys were
written; the i18n test wants every key in every language.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#5149's handle_chat_message_error had no tracker to tell. With one, a signed message that
failed validation never reached it, and the client's last-seen window drifted from the
server's. It now marks the signature as ChatListener.handleChatMessageError does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ChatScreen.handleChatInput` and `ChatScreen.normalizeChatMessage` had no
verdict at all: the ledger reads them as unported rather than as the port they
are. Both point at what the message becomes on the way to the server now.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
A chat line went out as typed: Horizon sent the raw box text, where vanilla
sends `ChatScreen.normalizeChatMessage(s)` — `String.trim`, then
`StringUtils.normalizeSpace` (one space for every run of whitespace), then
`StringUtil.trimChatMessage` (the first 256 UTF-16 code units, the length
`ServerboundChatPacket`'s `stringUtf8(256)` decodes).
Two player-visible differences:
- `send_chat` cut the message with `chars().take(256)`, so 256 emoji passed as
512 UTF-16 units and the server refused the packet; a cut at 256 units is
what vanilla does (129 emoji become 128).
- runs of spaces stayed: `hello there` reached other players with its three
spaces, and a line of nothing but spaces was sent, where vanilla sends
nothing and remembers nothing in the history.
The normalization sits in `Game::handle_chat_input` (ChatScreen.handleChatInput),
so the chat box, the chat drafts and the control socket's `chat` all send what
a vanilla chat screen would. `java_whitespace` is reused from `ui.rs`, so
U+00A0 stays (Java whitespace excludes the no-break spaces) and U+3000 and
Java's U+001C..U+001F separators collapse.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
`EntityType.updateCustomEntityTag` was ported for a player's egg
(`block_entity_item::spawn_egg_entity_data` then
`creatures.rs::Game::apply_spawn_egg_entity_data`) but not for the
dispenser's, whose `dispense_spawn_egg` stops after
`entity_nbt::apply_item_components`: a configured egg
(`/give @s pig_spawn_egg[minecraft:entity_data={…}]`) dispensed onto a block
spawned an unconfigured mob, which is the half of the
`EntityType.updateCustomEntityTag` row that pointed at
`game/dispenser.rs`.
`apply_spawn_egg_entity_data` becomes `pub(super)` and the dispenser calls it
after the components, as the player's path does.
`tools/port_map/game-005.tsv`: that row is `port` now.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
The anticheat's Attack arm and #5123's arm that keeps moves queued behind
an attack both landed in process_inbox; the second was unreachable, so
clippy -D warnings failed on main and #5123's behaviour was lost. The
anticheat arm now also holds the queued move when it forwards the attack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
f2a99d623 split verify into python, clippy and tests jobs plus a gathering
verify: four GitHub-hosted runners per checks run instead of one. With every
pull request push and every merge to main starting checks, that filled the
organisation's shared runner pool, and every workflow's jobs (labels, overlap
notes, other pull requests' checks) queued behind it for tens of minutes.
verify runs the public-tree check, the Python tests, rustfmt, clippy and the
Rust tests one after another again, on one runner. A pull request's
public-tree check still reads only its own commits (--range base..head).
Closes#5302
Co-Authored-By: Claude Opus 5.5 <[email protected]>
naga's GLSL frontend refuses three things GL drivers take, and the program was skipped:
- a swizzle of a scalar (GLSL 4.20: `noise(p).x`, `s.xx`; Kappa's weather map);
- a matrix column's component written to (`uv[1].y = ...`);
- `min`, `max` or `clamp` of a `uint` and integer literals (`max(n, 1)`): naga finds no best
overload, GL takes the uint one.
compile now rewrites the spot naga points at and parses again (at most 256 times): one swizzled
component is dropped, more become a vector constructor of the scalar, a column's component
becomes an index, and the call's integer literals get a `u`. Error locations map through the
rewritten text, which keeps its lines.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main. Main's 45c36d872 had added the client side of the same rule (RiddenBoat in motion/fluids.rs), so I kept main's type and logic and this PR now fills it for served players (lan_actions.rs → boats.rs riding_boat_fluid_box), where the air supply and drowning are decided; port map row updated. cargo check -p horizon-client --bins passes, fmt clean. Approved by rdbtCVS.
horizon-mods gained the zip crate, but the registry's separate lockfile
wasn't updated, so Railway's `cargo build --locked` refused to update it
and every deploy of horizon-mod-registry failed (one failure mail per
merge to main). Minimal update: only the new packages are added.
Checked: the Dockerfile's exact build (same files copied, --release
--locked) finishes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff: on focus loss a held (non-toggle) sneak/sprint/attack/use mapping is released before the re-sync, toggles keep their latch (ToggleKeyMapping.setDown/release); each frame a Shift/Control the keyboard's modifier state reports up clears its stale physical key and held latch. CI failures were main's Audio build break (#5251).
🤖 Agent: merged main (clean) on top of Domanffe's 2b2c2893e, which fixes dimaniojk's two findings (drag off the strip, the CLI's hue); earlier CI failures were main's broken Audio initializer. Launcher-only; cargo fmt clean. Approved by rdbtCVS.
🤖 Agent: F3 Density line as NoiseBasedChunkGenerator.addDebugScreenInfo under the chunk_generation_stats entry, sampled on a new feet block from the server thread's generator; main merged (conflicts with the per-entry F3 and explorer maps resolved, both kept), horizon-client checks clean; rdbtCVS approved.
🤖 Agent: Shader API: the material stage gets opaque_depth/opaque_color copies taken before translucent terrain and MaterialInput.screen (fragment position over resolution, as Teiwazik showed); rdbtCVS approved after merging #4186's tags; main merged. Only native shader packs see it.
🤖 Agent: merged main into the branch (main's broken Audio initializer made the earlier CI fail, fixed on main by f80c0360d); cargo check -p horizon-client-render passes. Change read: two adapter passes, GPUs first, software only with a warning. Approved by rdbtCVS.
🤖 Agent: functions tick before the levels (MinecraftServer.tickChildren) and an item added this tick first ticks on the next (EntityTickList), so a fresh drop reports Age 0; the freeze dimaniojk found is fixed (only items with a later id are skipped); type_specific/player checks ServerPlayer and input.sneak on the server. rdbtCVS approved; main merged.
The lid animated but the collision queries always answered "no box": a
closed box collided as its plain block and an opening one never grew. The
collision view now reads the boxes' lid progress, the level ticks those
lids on its server pass (not only the client-only animation), and an
opening lid applies moveCollidedEntities to the bodies the game moves.
Vanilla: ShulkerBoxBlock.getCollisionShape / getShape (isClosed),
ShulkerBoxBlockEntity.getBoundingBox / getProgressDeltaAabb /
moveCollidedEntities.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: merged main (port map: main's suspicious stew rows kept, this PR's variant rows). Mooshroom variant component as MushroomCow.get; test fails without it.
🤖 Agent: read the diff: placement checked before the spawn-list lookup (both pure reads, no RNG), species placement cached in MobData, LocalMobCapCalculator's lists flattened; draws and positions unchanged; A/B on playhorizon −18 % spawn_natural. CI parity runs on the head merged into main.
🤖 Agent: perf_bench --table prints the missing-value marker when a report has no scene-load measurement, keeps a measured 0.0 and the mean of measured scenes; regression test covers all three. Tooling only; rdbtCVS approved; CI green. Marked ready from draft.
🤖 Agent: verified: horizon-launcher completions <shell> via clap_complete over the existing command tree, no network or file writes, test per shell; approved as code owner. Thanks!
🤖 Agent: verified: vanilla's ENTITY_SHADOW / OIT_ENTITY_SHADOW pipelines take RenderPipeline.Builder's default cull(true); the shadow quads wind +Y (test added), so the underside is culled. Thanks!
🤖 Agent: verified against NbtIo.readUnnamedTag (StringTag.skipString): a file root's name that isn't modified UTF-8 reads as empty; keys and truncated names still fail. Proof: CI parity. Thanks!
Merges origin/main, moves the living_incoming_damage hook to
tools/native-hook-fragments (main's API stays as is), and drops the attackers
a canceled hit noted, so the next hit of that damage type isn't credited to
them (arismoko's review).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
nbt.rs: the Writer keeps both flags, this branch's empty_long_strings
and main's canonical_nan (both on for the disk writer, off for the
network one).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Q over a slot and Q in play decided whether to throw the whole stack from
physical_keys, while every other screen check reads Control from the OS
modifier word (has_control_down). The two could disagree after a lost or
lagging key event. drop_whole_stack now takes the modifier word; a latched
sprint toggle still never counts as Control.
The control socket's `release` cleared keys, physical keys and modifiers
but not the sneak/sprint toggle mappings, so a sneak pressed through
`key shift down` came back on the next sync_toggle. It now releases both
mappings as their key's release does (a toggled one stays latched) unless a
controller still holds the action, then resyncs the held keys.
Also:
- `key` reports an unknown action, with its name, from the single early
check; the later match arm that repeated it could no longer run.
- lan_e2e check_recipe_shift reads the recipe buttons before the tab loop,
so a book without a 35x27 tab fails on its assertion and target dump
instead of a NameError.
Validation on macOS arm64:
- cargo test --locked -p horizon-client --bin horizon-client: 359 passed,
3 ignored (including stack_drop_requires_ctrl_even_with_sprint_latched,
now driven through modifiers::set_key).
- cargo clippy --locked -p horizon-client --lib --bins --tests
-- -D warnings, cargo fmt --all --check, lan_e2e.py syntax and
tools/check_public_tree.py: passed.
Still to run with prepared resources: lan_e2e --check recipe-shift
--check modifier-release, and Ctrl+Q in an inventory and in play.
Refs #4166
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Screens now read Shift from the OS modifier word (has_shift_down), but two
synthesized shift-clicks still only inserted ShiftLeft into physical_keys:
the controller's Y quick-move in an inventory (pad_menu.rs) and horizon-ctl's
`station ... shift` click (station_control.rs). Both became plain pickups.
They now go through modifiers::hold/restore, which sets the SHIFT bit with
the physical key and afterwards puts back exactly what was held before:
a Right Shift already down, or an OS word set without a physical key, stays.
The control socket's `key <mod> up` returned before releasing the modifier
when a server dialog or the sign editor was open, or with no world, leaving
SHIFT set so every later inventory click became a QUICK_MOVE. The release
that already ran after `tap` whatever the handler did now runs after `up`
too (released_modifier), so no early return can keep a modifier down.
Tests: modifiers::a_held_click_restores_the_previous_state and
control::taps_and_ups_always_release_their_key.
Validation on macOS arm64:
- cargo test --locked -p horizon-client-input --lib: 64 passed.
- cargo test --locked -p horizon-client --bin horizon-client control::tests:
passed.
- cargo clippy --locked -p horizon-client-input -p horizon-client --lib
--bins --tests -- -D warnings, cargo fmt --all and
tools/check_public_tree.py: passed.
Still to run on a machine with prepared resources: controller Y on a slot
in the survival and creative inventories, and lan_e2e --check
modifier-release --check clicks.
Refs #4166
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read Shift and Control for screens from the OS modifier word instead of
inferring them from cached physical key events. This follows Minecraft
Java 26.3 InputWithModifiers.hasShiftDown/hasControlDown and prevents a
missed key release from turning subsequent inventory clicks into transfers.
Reconcile lost releases only for physical modifiers that were recorded as
held and are still assigned to the affected sneak/sprint mapping. Preserve
rebound non-modifier keys, controller-held actions and keyboard/controller
toggles. Include controller releases delayed until the next simulation tick.
This avoids cancelling crouch bound to C when the player presses Alt, or
cancelling a controller hold because the keyboard does not hold Shift.
Keep synthesized modifier presses and releases consistent with the OS word.
Releasing one side preserves the bit while the other side is still held.
Validate control key actions before mutating state. Run tap cleanup after
the command handler even if it returns early or fails, and clear the modifier
word on the control socket's release command. This prevents key shift tap
or key shift down followed by release from leaving later clicks modified.
Add five input tests covering modifier press/release, left/right holds,
rebindings, missing physical keys, controller holds and controller toggles.
Add the modifier-release LAN check: after a Shift tap or release, a normal
inventory click must pick up ten diamonds and closing must preserve the
server's count. Remove the obsolete pad-toggle getters introduced by the
previous broad reconciliation, since ToggleKeys already handles toggles.
Validation on macOS arm64 on main 9069aea3e:
- cargo test --locked -p horizon-client-input --lib: 63 passed.
- cargo clippy --locked -p horizon-client-input --all-targets -- -D warnings:
passed.
- cargo clippy --locked -p horizon-client --lib --bins --tests -- -D warnings
-A unreachable-patterns: passed. The allowed lint is main's duplicate
S::Attack arm in lan/hub.rs, a file this branch does not touch.
- cargo fmt --all --check, Python harness syntax, git diff --check and
tools/check_public_tree.py: passed.
Prepared Minecraft resources are absent here. The new modifier-release
check and final game-level verification remain pending on the dedicated
machine; prior LAN results must not be treated as validation of this change.
Rebuild binaries from this commit, then run:
python3 tools/lan_e2e.py --check modifier-release --check clicks \
--check shift-double-click --check recipe-shift --check shift-drag \
--check crafting --check wheel --out .local/validation-4166-review
Also rerun pr_ready.py with the appropriate main remote as its base.
Manual checks still required: hold rebound crouch/sprint while pressing
and releasing Alt; repeat with controller holds and toggle modes; confirm
lost Shift releases still recover with the default sneak binding. Verify
Shift taps and release before normal inventory clicks. Do not use the
deprecated in-frame path; keep game, launcher and XDG homes in fresh
scratch directories as required by AGENTS.md.
Refs #4166
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
A carried stack already starts QUICK_CRAFT in Clicks::press, but
inventory_press only initialized its hovered-slot collection without Shift.
Holding Shift therefore prevented the distribution gesture from completing.
Start the drag with either modifier state. Keep PICKUP_ALL restricted to
non-Shift double-clicks, and leave the Shift drag initially empty so a
press/release without any dragged slot retains its QUICK_MOVE behavior.
Follow Minecraft Java 26.3 AbstractContainerScreen.mouseClicked,
mouseDragged and mouseReleased; use the existing menu and server click path.
Add three LAN checks driven by real pointer events, rather than click-slot:
- shift-drag: spread 20 diamonds across three crafting slots as 6/6/6 with
two carried, or 1/1/1 with seventeen carried, with and without Shift.
- shift-double-click: transfer chest stacks of 8, 13 and 21 diamonds while
keeping a carried stack of 32; spaced clicks transfer only the first pile.
- recipe-shift: place two diamond-block recipe sets from 20 diamonds using
the recipe book, then Shift-click the result to produce two blocks and
retain two diamonds.
The latter two behaviors already worked and receive regression coverage.
Resolve pointer targets from the current UI and include movement inside the
starting slot so the drag actually traverses all selected slots. Check
carried stacks, grid contents and server inventory counts after closure.
Evidence reported by the dedicated-machine agent before this rebase:
Shift drags failed on the old base and passed after the fix; clicks,
shift-double-click, recipe-shift, shift-drag, crafting and wheel passed.
The container_screen, menu_click, place_recipe, recipe_book and
recipe_book_view differential harnesses were also reported passing.
Vanilla expectations came from the pinned sources and Java oracles; the
vanilla graphical client was not exercised. Those game runs cannot be
repeated on this checkout because its prepared resources are absent.
Refs #4166
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
Conflicts: datapack.rs takes both sides' kinds (trade_set,
villager_trade and dialog); game.rs saves through main's
write_level_for_role, then each placed player to their own file;
lan.rs syncs dialog ids, then sends the world's own biome table.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PiglinBrute.wantsToPickUp takes only a golden axe, through Mob.aiStep's
looting loop. StartAdmiringItemIfSeen sets ADMIRING_ITEM (119 ticks) for a
loved item in sight, and StopAdmiringIfItemTooFarAway(9) and
StopAdmiringIfTiredOfTryingToReachItem(200, 200) end it. A blow from a
player disables admiring for 400 ticks (ADMIRING_DISABLED).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Only the shadow passes write the shadow targets, so while the key matches they
still hold the map (and the A/B side shadowcomp left current): no cache
textures, no copies, and no copy into shadowcolor buffers made without
COPY_SRC/COPY_DST (a wgpu validation panic with any pack using shadowcolor).
pass-timing leaves this PR (unrelated).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read the diff at 7fc284b (rdbtCVS's main merges + Escape-as-Cancel fix): the splitscreen profile page as a model with its store and tests in horizon-client-render, not wired into play yet; dimaniojk's paging finding fixed. Part of the splitscreen series whose earlier parts merged.
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff: an items-only content mod now writes the identity marker, and the identity hashes the mods' item and mob effect entries per registry; a root without mod entries keeps its block-only hash (unit test). Fails safe with the content-mods mismatch message across versions.
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. two spans (spawn_natural, tick_creature_entities) inside tick_creatures, timed only when spans are on; no behaviour change. CI failures were main's Audio build break (#5251).
🤖 Agent: merged main into the reviewed head (the earlier clippy/tests failures were main's Audio build break, fixed by #5251); no other change. read the diff against vanilla: spear is in #minecraft:no_knockback, so LivingEntity.hurtServer skips dealDefaultKnockback for a stab; Damage::no_knockback now lists Spear; unit tests. CI failures were main's Audio build break (#5251).
🤖 Agent: merged main (both sides added tests at the end of mod_content_tests.rs: kept both); cargo check of the lib and tests passes; review points fixed by the author (02a7c1553 cherry-picked); marked ready
Launcher: Language::Turkish (code tr, Türkçe, picked from a tr-* system
locale), tr.json brought up to main's strings (25 native-mods strings
translated, 5 the launcher no longer asks for dropped, one {version}
placeholder fixed). Game: tr_tr.json joins the bundled Horizon strings.
Built on #5035's Japanese wiring so the two don't conflict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: finished by me (94027c278): merged main, API number as a fragment, the canceled-join crash fixed (just_spawned_item), and a same-level End gateway trip no longer joins again. native_ tests pass.
🤖 Agent: the review follow-ups from #3996–#4289 (screen only for the block's own menu, entity menus clear the block target); review points answered; marked ready
🤖 Agent: read the diff: Superflat layer and preset icons draw the item's GUI model (as CreateFlatWorldScreen's renderItem), clipped exactly to the list; other screens unchanged. Approved by rdbtCVS; CI green, parity runs on the head merged into main.
🤖 Agent: one partner_hidden setting hides the sidebar card and the Servers banner; the 3-minute seen gate now counts on every page. rdbtCVS approved; launcher CI green.
🤖 Agent: rename-aside Windows self-update with rollback replaces self_replace's self-deleting copy that Defender flags; start-up clean-up removes only this exe's .old/.update leftovers. Read in full; rdbtCVS approved; CI's launcher job builds Windows. A real Windows update after release is the remaining check.
startRiding(vehicle, true, true) skips canAddPassenger, so a boat under
water still takes the rider; Horizon's boats seat at most max_passengers,
so a full one still refuses. Horizon's minecarts carry no mobs yet, said
in a comment. A player put in a boat or minecart now fires
minecraft:started_riding (sendEventAndTriggers).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#5235 keeps the address in horizon_telemetry::server_status::OFFICIAL_ADDRESS:
it is now play.playhorizon.dev, with OFFICIAL_PREVIOUS beside it; the
launcher's defaults and the game use both. The launcher's official player
count pings the address from links.json through its SRV record
(horizon_protocol::server_redirect + server_status::players_at): port 25565
on that host is another server. The telemetry server, which looks no SRV
up, defaults to the SRV target (OFFICIAL_STATUS_ADDRESS). The game moves
servers.dat entries for an earlier address once, like the launcher.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pushAndPickupEntities only runs for a rideable cart moving at
horizontalDistanceSqr >= 0.01; an activator rail ejects mob passengers
too; a player can't board a cart a mob already rides (isVehicle).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A failed signature and failed OS randomness are logged instead of hidden, and the
ChatListener.flushQueue ledger row points at flush_chat_queue.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4935 landed as API 45, and API numbers now come from
tools/native-hook-fragments (stamp_native_hooks.py after the merge): the
hand-written API 44 and its docs paragraph become
native-block-entity-sync.json. lan_bot keeps both new tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review fixes on the preparing screen:
- The worldgen data was read from the bundle alone, so the packs the Data
Packs and Experiments screens picked never reached the generator (the
cached vanilla `GeneratorData` came back). The read goes through
`shared_data_with` now, with the overrides the chosen packs build
(`creation_worldgen_overrides`, the worldgen companion of
`applyNewPackConfig`'s reload), and a failure reports on the title's
error screen instead of only logging — the Create screen's options stay
(`failed` keeps the mode as the back target).
- Escape and text input are ignored while `preparing` is up
(`GenericMessageScreen.shouldCloseOnEsc` is false): Escape during the
read used to leave Create and `finish_preparing` then replaced it with
a fresh screen, losing the player's options.
- The confirm screen's Back/Escape set `reopen_data_packs`, but nothing
read it: the game side polls it now and reopens the Data Packs screen
with the selection it had (`onAbort`).
- The preparing assertion in the navigation test is unconditional, and
offer_data_packs (with and without a feature pack), the confirm
screen's Proceed and Back, and preparing's input blocking have tests.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
The official server moves to play.playhorizon.dev (SRV to port 24454).
launcher/links.json now carries `official_server` and
`official_server_previous`, read through the existing Links mechanism with
the same built-in defaults, so a later move is a links.json edit, no release.
server_list::add_official offers the address from links, and when the
address differs from the one the marker file recorded, rewrites entries for
a previous address to the new one (player's name, picture and fields kept),
once. No duplicate when the new address is already listed; a player who
removed the server is not offered it again. The Home list re-runs it when
links.json arrives. The game's own built-in copy and docs/smp.md follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The page stays closed after its last mesh moves, so the frame's uploads can't
refill it before trim returns it (trim clears closing). Pages record whether
their buffer went to system memory: moves only go to video-memory pages
(alloc_moved), and sparsest empties a system-memory page first. Tests for both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The hook asks for its API number with tools/native-hook-fragments/entity_leave_level.json
(#5161); NATIVE_HOOKS_API, the histories and native-hooks.json are main's again.
- An End gateway keeps its traveller in the level (teleportSameDimension), so an item
going through one no longer reports leaving it (arismoko's and rdbtCVS's point).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4186's precipitation/temperature/downfall/wetness come first in Frame (they are on main);
this branch's frame-motion fields follow at 416..512. Reference regenerated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three things kept tools/lan_e2e.py (and agent_control_e2e.py) from running
on Windows:
- The prepared resources were linked into the scratch home with os.symlink,
which needs Developer Mode or an administrator (WinError 1314). The new
lab.link_dir falls back to a directory junction there, which needs
neither; shutil.rmtree removes a junction without touching its target.
- agent_control_e2e.game_env ran `vscreen up`, which doesn't exist on
Windows. Windows has no virtual screen, so it takes plain lab.game_env,
as macOS does.
- `horizon-ctl launch` never returned when its output was captured:
Windows hands a child every inheritable handle of its parent, so the game
kept the caller's stdout pipe open until it quit. horizon-ctl now clears
the inherit flag on its own standard handles before it starts the game
(horizon_threads::keep_stdio_from_children). Linux and macOS are
unchanged.
Fixes#5274
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's 45c36d872 added RiddenBoat for the client's own fluid box. The
served player's tick (lan_actions.rs) now fills the same type from the
ridden boat, so the server-side air supply follows it too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The hook asks for its API number with tools/native-hook-fragments/entity_join_level.json
(#5161); NATIVE_HOOKS_API, the histories and native-hooks.json are main's again.
- A mod canceling an item's join no longer crashes a throw: spawn_item's callers
touch the new item only if it joined (just_spawned_item), not whatever item is
last (throw_from_eyes unwrapped it; allays, shearing and /give's overflow
edited an unrelated item).
- An End gateway keeps its traveller in the same level (teleportSameDimension), so
the item doesn't join again on arrival (arismoko's review).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_mobs is half of a crowded server's tick, and its two big parts
(NaturalSpawner and the entity ticks) move independently with the players'
spread. Two nested spans let the slow-tick log and horizon-report tell them
apart; with spans off they cost one relaxed load each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's AbstractContainerMenu.clickMenuButton(Player, int) reaches the player's carried stack;
native_menu_button(pos, entity, id) does not, so a mod's widget outside the slots (a tank taking a
bucket) cannot take or give what the player carries. native_menu_button_v2 fires right after it with
`carried: &mut Option<ItemStack>`; a change is written back and sent to the player (SetCursorItem).
Single player's player is a guest of its own host, so one path covers it and LAN guests.
native_menu_button is unchanged (frozen). The API number comes from the fragment when stamped.
Closes#5246
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Wire ja.json into Language (code ja, name 日本語), translate the native
mods strings ja.json and pt-br.json lacked and drop the five strings the
launcher no longer asks for, so every language has every string again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lenientOptionalFieldOf("enabled_features", DEFAULT_FLAGS) omits the tag
when the set is vanilla's default, so a `["minecraft:vanilla"]`-only list
(a world created with no experiment picked) does not reach level.dat;
Horizon wrote it. The round-trip test the body promised is here too: a
picked experiment's list writes and reads back equal, a vanilla-only
selection writes no tag.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
for_recreate filtered the saved selection against the world's own packs
only, so a world with trade_rebalance on was re-created without it.
Vanilla's recreateWorldData keeps the DataConfiguration. rdbtCVS's review
point 3 on #4619.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review fixes on top of the hue strip:
- A drag released outside the strip now writes settings.json too. on_mouse_up
only fires while the element is hovered, so the accent stayed in memory and
was lost on restart; on_mouse_up_out covers the release off it, and
hue_dragging keeps a click elsewhere on the page from saving.
- The contrast test sweeps every half degree from 0 to 359.5 on both bases
instead of two hand-picked hues, so the guarantee is tested where it is
claimed.
- `settings accent <value>` names the degrees a value may be, not only the
seven names.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Resolve title.rs (Outcome::Connect gained `lan`), handle main's new
graphics_ui::Action::Calibrate in the wheel and key paths, and let
Escape close Video Settings on the first press.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ExperimentsScreen's switches wrote `options.data_packs` on every flip, so
Cancel and Escape left the flips in place — vanilla's ExperimentsScreen
only flips its own list and writes the selection back in `onDone`. The
switch now flips the screen's flag only; Done rebuilds the selection the
way `onDone` does (the other packs keep their order, the selected
features are appended in reverse screen order), and Cancel drops it.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
main added three strings to every language (`finding_friend`,
`opening_world`, `cancelling_world`); es_es keeps its 198 translations
and gains those three, for 201/201 keys in en_us order.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Handle root-array and modList metadata with synthetic ZIP regressions.
Preserve modern metadata precedence and existing fallback behavior.
Co-Authored-By: Codex <[email protected]>
Conflicts: main's lan login fallback (an auth failure only warns on LAN) kept
with this branch's keyed reasons; reconnect tests take both the lan flag and
the reason's key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ports the 26.3 DebugEntry* classes the debug options screen listed but
the overlay never drew: memory, targeted block/fluid/entity state and
tags, chunk render/source stats, entity and particle stats, player
speed, light levels, heightmaps, biome, local difficulty, day count,
spawn counts, sound mood and cache, post effects and GPU utilization,
in DebugScreenEntryList's order (by identifier) with vanilla's text and
priority/line/group placement. The integrated server reports the lines
that read the server level.
Renderers: water levels, heightmap, collision boxes, block and sky light
levels, solid faces. The list's scrollbar can be dragged, and
debug-profile.json gets vanilla's three datafixes by DataVersion.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`x / 0.0` and `sqrt(-1.0)` are GL's infinity and NaN at run time (Mellow's vec4(-1.0 / 0.0),
Bloop's sqrt of negative constants). naga's frontend folds them into constants and refuses the
result, so the program was skipped. They now divide by `hz_zero`, a private float the frontend
can't fold: `x / hz_zero`, and `hz_zero / hz_zero` for the square root.
texelFetch1D/2D/3D (GL_EXT_gpu_shader4's names, Bliss) become texelFetch, as texture2D and
the other old names already do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts in options_ui.rs (main's HDR calibration page beside the profile
page) and lib.rs's re-export list: kept both. Escape on the list now sets
choice = Some(None), as the Cancel button does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two readers of the packs' tag files never saw any file:
- `PackOverlay::tag_kind` stripped `":{registry}/"` from a path that
`split_once(':')` had already freed of the colon, so it matched nothing
and every pack tag merged into `LootData` (item, enchantment,
instrument, entity_type, damage_type) was silently lost. It strips
`{registry}/` now, with the key shapes `content_id` writes fed through
it in a test.
- `load_tag_lists` (horizon-items) read the same key shape through
`overlay.kind("tags/{kind}")` and hit the same nothing: a pack's item
and block tags never applied. It reads `tag_kind(kind)` now.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
AABB.ofSize(center, size, size, size), not the box grown by its size on
every side, which let a buried orb jump three times as far as vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep command signing disabled until the server command tree arrives, count chat length in UTF-16 units, and acknowledge delayed chat during configuration transitions.
Co-Authored-By: Codex <[email protected]>
network_stack (integrated-server and LAN inventory sync) encoded custom_name
and item_name from StackData.other but skipped minecraft:custom_data and
minecraft:custom_model_data, so item models' tints/selectors and client-side
custom_data reads saw nothing for a synced stack. Push both components, built
from the saved tag the same way vanilla's DataComponentPatch carries them;
item_sync's decode side already mapped both back.
Co-Authored-By: Claude Sonnet <[email protected]>
Vanilla's DownloadedPackSource puts active server packs in the selection
as required, fixed at the top (pack.source.server). Horizon already
applied them but never listed them; they also stay out of options.txt.
Fixes#5218
Co-Authored-By: Cursor Agent <[email protected]>
Ludovic (2026-10-31) asked to add x3 кто (Discord vladislav_hagen,
GitHub Qwerner88880, the native-mod hooks author).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A kill leaves the old id in the UUID map, so the first match could be an entity the level no longer holds. on owner follows getLivingEntity, which does not return a marker or a display.
Co-authored-by: Cursor <[email protected]>
NeutralMob.readPersistentAngerSaveData hands the saved anger back through
Mob.setTarget, and setTarget runs asValidTarget, which turns a target the
mob cannot attack into no target at all. IronGolem.canAttack refuses every
player when a player built the golem. Horizon's world load wrote the target
directly, so a player-built golem reloaded from a save kept its builder as
its target, and its melee goal never re-checks canAttack.
The golem rule becomes Mob::built_by_player, used by both the restore and
spares_players, with tests pinning the targets a built golem does and does
not take and the crack sound it already plays.
Preserve the client's destroyBlockPos after a completed prediction so a replacement at the same position can resume the retained target, matching the isDestroying split.
Co-Authored-By: Codex <[email protected]>
handleBlockBreakAction writes destroyPos in the branch that keeps digging,
after the creative destroy and the instant-mine check, so a creative or
one-hit START leaves no block behind for a later STOP to break.
Reported in review: destroy_pos was set for every accepted START.
MultiPlayerGameMode.stopDestroyBlock opens with `if (!isDestroying) return`,
so it zeroes the progress once, on the stop that ends a running break; every
later missed pick is a no-op, the dig identity survives, and the client keeps
accumulating until the progress passes 1.0 and the STOP it sends is measured
from the first START.
Clearing the accumulated progress on every miss restarted the dig each jump,
so the client never reached 1.0 and the server's retained clock was never
read. The LAN regression now breaks the stone in 100 ticks with the dig
accumulating to 27 ticks across falls.
The verifier asked for the server-thread regression this change needs: a
survival guest under a block it can only reach near the jump apex, holding
attack and jumping, with the block's dig clock measured from the first
START.
It currently fails: the block survives 308 ticks of held attack. The
client aborts on every fall and the STOP that would read the retained
clock is only sent when the client's own progress completes, which a
five-tick in-reach window cannot reach. Reporting rather than papering
over it.
tests/test_remote_mining.py compiles an extracted copy of
stop_remote_mining against a stub, so the stub needs the new flag and its
assertion must stop expecting a stop to clear the target, which vanilla's
stopDestroyBlock does not do.
A click on the block a stopped break still names has to start the dig
again, so the server re-anchors its clock, while a held button carries the
same target on and keeps it. That is vanilla's "isDestroying &&
sameDestroyTarget" guard: the target test stays pos plus item, and the
running flag is what the start guard adds.
The STOP decision moves into a small pure function over the retained
destroy_pos, the elapsed ticks and the block's per-tick progress, so the
0.7 break, the delayed arming from the same anchor and the cases that must
not inherit a clock are covered without a Game.
Vanilla keeps ServerPlayerGameMode.destroyPos and destroyProgressStart after
an abort or a STOP that arrives out of reach: only a START writes them, the
abort clears isDestroyingBlock alone, and tick does not check distance. On
the client, stopDestroyBlock keeps destroyBlockPos and destroyingItem, so
sameDestroyTarget still matches after a fall, no second START is sent, and
the server's clock keeps running. That is the "break it a bit each jump"
behaviour the report asks for.
Horizon had the formula but not the state: the ABORT and too-far STOP
branches cleared the dig, guest_stop_digging required an active dig, and
the client's own target died on every stop, so each landing re-sent START
and re-anchored dig_start.
The server now retains destroy_pos (written only by an accepted START,
cleared by a respawn) and measures a STOP from the START that named the
block. The client splits isDestroying out of the target, so the target
survives a stop while a running break is still the thing that suppresses a
new START.
GL drivers evaluate a float in #if as a number (I Like Vanilla's #define AMBIENT_CEL_AMOUNT 0.0
tested with #if AMBIENT_CEL_AMOUNT != 0), and Iris loads such packs; Horizon refused the line and
skipped the program. The evaluator now computes in f64: float literals count, a fraction is true,
and / and % stay integer division unless a float literal appears.
A condition that still can't be evaluated already reads as false, but it was a diagnostic, so the
whole program was skipped (Pastel). It is now a warning in the loader's notes, and the program
loads with that branch off, as Iris runs those packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
For a public server's SMP mod: native_sidebar shows one player a sidebar of
up to 15 lines (a text, and one where the score would be) that only their
client holds, native_tab_list sets the lines over and under their Tab
list, native_tick_stats reads /tick query's rate and average tick time,
and native_sleep_ignored leaves a player out of players_sleeping_percentage's
count until they are in bed. Server players now carry yaw and pitch, for
telling who is away from the keyboard.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerHeadItem.getName answers block.minecraft.player_head.named with the
profile's name, so a head given for someone reads their name rather than
"Player Head". Horizon showed the plain item name in the tooltip and in
command feedback.
A stem has no block item, so vanilla's pick-block returns the seed it grew
from. Horizon fell back to the block's own item, found air and picked
nothing, so middle-clicking a grown pumpkin or melon stem did nothing.
ShearsItem.useOn on a growing-plant head below its top age plays
block.growing_plant.crop, sets the head to age 25, fires a block change,
wears the shears and succeeds. Horizon had no such arm, so shears on kelp
or a vine did nothing.
The arm sits with the pumpkin carve and the beehive shear and reuses their
helpers.
`es_es.json` had 13 of the 198 keys `en_us.json` ships, so the other 185
fell back to English on every Spanish screen. Translate them, keeping the
13 existing strings and every `%s`/`%.2f` placeholder.
The file also serves `es_ar`, `es_cl`, `es_ec`, `es_mx`, `es_uy` and
`es_ve`, which `lang.rs` aliases to it.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
The same lost key release that latched sneak also leaves the physical
modifier down, and a screen reads the physical state: every inventory
click becomes a shift-click, every throw a whole-stack drop.
native_block_entity_sync(pos) puts the position in the level's block log,
as Level.sendBlockUpdated marks ChunkHolder.blockChanged: the hub sends the
block and the block entity's update tag to the players who see its chunk,
once per position a tick. NATIVE_HOOKS_API 24. A lan_bot test checks the
chunk's tag, one BlockEntityData for two syncs, and none without a tag.
Refs #4615
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Measured with a probe pack in Minecraft 26.3 + Iris 1.11.7 against Horizon on the same world and
camera:
- gbuffers_skybasic, gbuffers_skytextured and gbuffers_clouds get full light, lightmap
coordinates (240, 240); Horizon gave (0, 240).
- gbuffers_skybasic's normal is (0, 0, 1); Horizon gave (0, 1, 0).
- Clouds get their face's normal (down for the bottom, out for the sides); Horizon gave up for
every face. The cloud mesh now carries it (w 0: the game's own clouds still shade by colour).
- blockEntityId is -1 outside block entities; Horizon gave 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A release does not always reach the mapping: a screen reads presses only, and
an unfocused window is given no release at all. The latch then keeps the player
sneaking or sprinting until the key is tapped again, which is what a short
alt-tab did on Wayland. The modifier state is what the keyboard itself asserts,
and winit reports it before any screen or gameplay handler runs, so a per-frame
check settles the question instead of waiting for a release that already got
away.
A mapping that toggles, or one a pad latched, is left alone: set_down(false,
needs_toggle) is a no-op for those, and a toggle the player asked for is meant
to outlive the key. A mapping rebound away from its modifier is skipped too,
since the modifier says nothing about it.
A cauldron's drain now hands out only whole thirds that fit within the
amount asked, as IFluidHandler.drain's maxDrain: asking 1 or 500 of a full
one takes one third (334) or none, so a caller moving into the room it has
never receives more than it can hold. Its fill compares each level's whole
third against what is left of the amount instead of summing, so a huge
amount can't overflow. A save with more tanks than the block now declares
reads the tanks that still exist instead of all empty, which the next write
would have made permanent.
Review: Aria's agent swarm (Dyna) in the pull request.
Co-Authored-By: ZCode AI agent (GLM-5.3)
- A position's placement (isSpawnPositionOk) is tested before its spawn list
is looked up again (canSpawnMobAt: structures reaching it, the zoomed biome,
a string-keyed list). Both only read the level, so their order cannot change
the answer, and the few block reads of the placement turn most positions
away. The placement is read from a table by species, not by an id built
with format! after a linear search (from server-tick-2's
perf/spawn-placement-first).
- LocalMobCapCalculator's players near a chunk are runs of one list, not a
Vec per chunk, and the counts per player a Vec by player index, not a hash
map: thousands of chunks ask every tick on a full server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture exports vanilla's colours, whose alpha is 255: the fluid face in
the high nibble is Horizon's own, as the emission in the low one is. A test
now checks the material stage's decode against the mesher's encoders.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
A short alt-tab (under 500 ms, so no pause screen opens) leaves the player
sneaking until Shift is tapped again. The latch in `toggles` survives the focus
loss and #4139's `sync_toggle` puts ShiftLeft back into `keys`, while an
unfocused window never receives the key release that would clear it.
Release each mapping that does not toggle, so a held sneak, sprint, attack or
use cannot survive a focus change, and pass the option through for the ones
that do: `set_down(false)` is a no-op for a toggling mapping and for a pad
latch, so the latched state #4139 was protecting still crosses the alt-tab.
The layer list and the preset list drew the flat item/<name> sprite, so a block
showed as the first texture of its model. They now resolve the item and draw it
as GuiGraphics.renderItem does (isometric for blocks, buckets for water and lava),
clipped exactly to the list; the sprite stays as the fallback.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
HappyGhast.getControllingPassenger makes the first passenger control
a harnessed adult (wearing body armor, not on a still timeout), like
AbstractHorse's does for a saddled horse. client_moves only knew the
equine half, so on a Java server the client never moved the ghast and
never sent MoveVehicle: WASD did nothing.
Verified against the pinned 26.3 bytecode:
rea_vanilla HappyGhast.getControllingPassenger / isOnStillTimeout.
The frontier-starvation fix (accept a result newer than the one shown, even
while a newer job is pending) had no test: it ran inside `receive`, which only
the worker channel can reach. `accept_mesh` is that rule on its own, so the
test drives it directly — an older-but-newer-than-shown revision fills the
frame and leaves the tile waiting, the same revision is not redrawn, the
newest settles it, and a canceled result or one for a tile that left the view
is dropped.
Generated with DeepSeek Harness (deepseek-v4.1-flash)
Co-Authored-By: DeepSeek Harness <[email protected]>
A shaders/ pack's terrain program reads each quad's face normal and mc_Entity only in flat draws
(PackAtlas::terrain_inputs(flat)); otherwise it gets (0, 1, 0) and -1. Flat draws existed only
on Metal and Intel Gen11's Vulkan driver, so on NVIDIA and AMD every pack lit all terrain as if
it faced up and knew no block from block.properties. Flat draws now exist wherever the device
supports them; frames without a pack keep per-section draws outside Metal and Gen11
(flat::for_vanilla), so only a pack's terrain and shadow pass draw flat there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Switching from spectator to creative always left the player flying. In
ServerPlayerGameMode.changeGameModeForPlayer a flying player who leaves
spectator mode stops flying when isInRangeOfGround holds: no collider
touches the player's box and Entity.getAvailableSpaceBelow(1.0) is under
one block.
Orbs had no health and ignored blasts. Vanilla gives an orb 5 health
(ExperienceOrb.health) and ServerExplosion.hurtEntities calls
ExperienceOrb.hurtServer with the blast damage, which takes it off as an
int and discards the orb at 0 or below; pushFromExplosion then moves the
orbs that stay, from the eyes at 0.85 of the 0.5 high box. The health is
saved and loaded as "Health". Fire and lava damage are not ported yet.
Farmland that dried out or lost its support, and a path with a block over
it, became dirt and left whoever stood on it sunk a sixteenth into the
new block. FarmlandBlock.turnToBaseBlock and PathBlock.turnToDirt call
Block.pushEntitiesUp (Block.java): every entity touching the collision
the new state adds is teleported up so it rests on it. The level now
reports the swap and the game lifts the players, guests, mobs and items
the same way.
Only the player broke an egg by stepping on it. TurtleEggBlock.stepOn
(TurtleEggBlock.java) runs for every entity on the ground that does not
step carefully: canDestroyEgg refuses turtles and bats and needs mob
griefing for other living entities, then the level random rolls 1 in 100
and decreaseEggs plays the break sound (0.7, 0.9 + random * 0.2) and
removes one egg or the block. A mob now rolls that in its block effects
and the game cracks the egg.
A wind charge put a lit candle out with the fire-extinguish sound and no
block-change game event. AbstractCandleBlock.onExplosionHit calls
extinguish(null, ...) (AbstractCandleBlock.java): setBlock lit=false with
flags 11, the candle.extinguish sound at 1.0/1.0 and a block_change game
event; the smoke is a level.addParticle, which a server level ignores.
Use the world's extinguish_candle, which does exactly that.
A wind charge opened a gate by running the click path, which swings the
gate's facing towards the player when it faces them. Vanilla's
FenceGateBlock.onExplosionHit (FenceGateBlock.java) only toggles OPEN of
an unpowered gate with setBlockAndUpdate, plays the open or close sound
at pitch random * 0.1 + 0.9 and posts block_open or block_close with the
old state as context and no entity.
A ravager only trampled wheat, carrots, potatoes, beetroots and
torchflowers; PitcherCropBlock.entityInside (PitcherCropBlock.java) does
the same for a pitcher crop: with mobGriefing a server level destroys the
block with drops for any Ravager inside it.
Using an op's spawn egg with entity data on a trial spawner only set the
mob id and dropped the data. TrialSpawnerBlockEntity.setEntityData runs
TrialSpawner.overrideEntityToSpawn(TypedEntityData), whose
FullConfig.overrideEntityData / TrialSpawnerConfig.withSpawning build each
config's spawn data from the egg's data (TypedEntityData.loadInto), then
the state goes to inactive.
Horizon had only the empty-hand take-out of FlowerPotBlock, so no
potted plant could be made. FlowerPotBlock.useItemOn (FlowerPotBlock.java):
an empty pot clicked with a block item that has a potted variant
(getFullPot) becomes that pot, sends the block_change game event,
awards the pot_flower statistic and consumes one item; any other item
tries the empty hand.
ServerEntity.sendChanges (ServerEntity.java) updates a frame's map only when the framed item is a MapItem; lan_map_updates took any framed stack with a map_id component, so /give stone[map_id=1] in a frame got map packets. That check followed a NeoForge-patched decompile, which replaces the test with if (true).
native_entity_leave_level(dimension, entity, kind) fires as vanilla's
onTrackingEnd: wherever an item entity or orb is removed (picked up by a
player or a guest, merged, despawned, burnt, blown up, killed, taken by a
hopper or a mob, gone through a portal) and when its chunk unloads,
including a level closing empty. Single removals go through
remove_dropped / remove_orb; the ticks and sweeps compare IDs before and
after. Not told at a server stop, as in vanilla. API 41.
Part of #5162 and #4953.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
machine_screen() answered Hopper or Dispenser by the menu's type even when
a mod drew its own screen over it, so the player inventory and the menu's
slots sat where HopperScreen puts them (inventory at y 51), not where the
mod's screen said. It now answers none while a mod screen shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hovering an option, a screen or the profile button shows what the pack's lang file says of it
(option.NAME.comment, screen.NAME.comment, profile.comment). Shift-click puts one option
back at the pack's default. A label wider than its button shortens the option's name, never
its value, and the whole label shows on hover.
The options a pack lists in sliders are sliders, each step one of the option's values. const
bool declarations (const bool shadowHardwareFiltering = true;) are switches too; as they are
as often the pack's own settings, they show where a screen names them, not with *.
The options a pack declares (switches, values with their lists, consts) on the screens
shaders.properties lays out, with the names its lang files give them, its profiles, and
Reset. Choices are saved to shaderpacks/<pack>.txt and applied to the pack's sources as
they are read; leaving the page loads the pack again with them.
local_size_x = 8 * 2 gave gl_WorkGroupSize 8, and a size that is not a number 1, silently;
such a program now fails with a message. Without clear_texture, images clear from a zero
buffer of at most 4 MiB, copied a slice of rows at a time, instead of one as large as the
largest image.
A geometry stage's write wrapped as if (hz_primary) took the else of an unbraced if; it is
now a whole if (!hz_primary) {} else write. A barrier after a shared atomic no longer goes
between an if and its else.
An image whose load can run after the same invocation's store (a loop around both, or a
function holding both called in a loop or more than once) is no longer read from the copy
taken before the dispatch. That copy has a texture of its own, so a pass sampling last
frame's copy later in the frame still finds it.
It was skipped as writing no buffer; the sink was only given to fullscreen programs. A program
drawing geometry has the depth buffer, so it now draws with no colour target, its images
written as GL's would be.
A unit test folds a small shadow program with a geometry stage and checks its triangle count.
It found that inputs declared without a size (in vec3 v[]) were unsized arrays naga refuses;
they have the triangle's three elements. An image declared with its layout on the line before
no longer gets a second one.
eyePosition, playerLookVector and playerBodyVector came from the camera, which a third-person
view backs away from the player (and turns around in front view); they are the camera
entity's eye and rotation now, and relativeEyePosition is the eye from the camera.
A shared vector split for atomic functions took sums += v to hz_get_sums() += v, which does
not compile; compound assignments write through the setter. Array sizes of several tokens
are kept whole. Tests cover components, swizzles, whole reads and writes, arrays and a
program compiling end to end.
Clearing image.* happened after the shadow and shadowcomp programs ran, wiping what they
stored; it now opens the frame. A bufferObject.* larger than the device binds is made at the
largest size it takes, with a warning, and its per-pixel size no longer overflows.
gbufferModelView holds the nausea spin the world turns with; the hand's projection now takes
it out again, as vanilla's hand never spun. Tests cover bufferObject.*, image.*,
iris.features.* and buffer blocks with and without a binding.
A composite or deferred program that samples colortexN and stores to colorimgN now samples
a copy taken before it runs, as compute programs did; it sampled an empty texture. Whether
vanilla's sky and sun are drawn depends on the pack having the program, not on it building a
pass.
workGroups needs three numbers and workGroupsRender two (or three, the last taken as 1);
another count fails the program with that message instead of running it 1x1. size.buffer.*
factors must be finite and positive, and every size resolves to at most 8192.
Its size is checked against the device's largest texture of its dimension and its texels
against that size before anything is created, so a pack asking for too much loses that
texture instead of failing validation at upload.
naga's HLSL writer has no WorkGroupSize built-in, so on DirectX 12 the two Rethinking Voxels compute programs that read it were refused. It becomes the constant the program's layout(local_size_*) declares, as GLSL defines it.
GL leaves undefined what one invocation reads of what others of the same dispatch write, and packs lean on how desktop drivers happen to run it. Rethinking Voxels' light lists changed from frame to frame on Apple GPUs, so glass, which reads its light volume directly, flickered.
- An image a compute program both loads and stores, with every load before every store, no atomic function on it and neither coherent nor volatile, is loaded from a copy taken before the dispatch: neighbouring work groups' light lists, reprojection in place and the volume shift read the values they expect.
- A barrier follows each statement of a compute main that applies an atomic function to shared memory, directly or through a function, unless main returns early: light weights raised with atomicMax were read back before every invocation had raised them.
- Flat terrain draws a program with a geometry stage without its index buffer: the vertex stage reads it (set 5) and the draw has an instance for each triangle the geometry stage may emit.
- Programs that write only images draw into a sink that keeps nothing.
- The pack's images are cleared before the shadow pass, not after it, and a pass that samples an image it stores samples last frame's copy. Images an atomic function uses are made for atomics (Metal dropped them otherwise), and images bind in vertex stages where the device lets them.
- HORIZON_SHADER_PACK_DUMP=<image> logs how many of a pack image's texels are set, for debugging.
Rethinking Voxels r0.1-beta9 voxelizes the world in its shadow pass and lights it with its colored voxel lights.
A shadow program of terrain with a geometry stage (shadow.gsh): wgpu has none, so its vertex, geometry and fragment stages become one vertex stage and the fragment stage (translate_geometry). Each vertex invocation stands for one corner of one triangle the geometry stage may emit: it reads its input triangle's three vertices itself (GeometryInputs says how), runs the pack's vertex stage on them, then the geometry stage, and keeps its corner.
- The shadow pass has the cutout test (GREATER 0.1) unless alphaTest.<program> says otherwise: leaves cast the shadow of their texels, not of their blocks.
- Images declared without a format take the one image.* gives them.
- gl_VertexID became hz_VertexID: the compatibility inputs' prefix gl_Vertex matched it first.
The game gives no GeometryInputs yet, so such programs are still skipped.
cameraPositionInt and cameraPositionFract (and their previous values) are the camera's whole blocks and the rest, exact however far from the origin; eyePosition, relativeEyePosition, playerLookVector and playerBodyVector are the eye and where it looks, the camera being the eye in first person. The previous camera position is kept in double precision for them.
- bufferObject.<n>: a storage buffer of n bytes (or bytes per pixel of the render size). Buffer blocks are bound in set 4 and lose their access qualifiers so that every stage binds them alike.
- image.<name>: 2D and 3D images of the pack's, with their format, size (or the render size scaled), sampler name and whether they clear before each frame.
- iris.features: SSBO and CUSTOM_IMAGES are supported.
- A 3D image (iimage3D) is declared as 3D.
- naga: atomicAdd and the other atomic functions on buffers and shared memory, imageAtomic*.
Nothing binds them yet.
- Compute programs dispatch before their composite-style program, over workGroups or workGroupsRender times the target's size; their images bind the side colortexN reads, and a pass that samples and stores one buffer samples a copy taken first. The MC_GL_KHR_shader_subgroup_* macros are defined when the device has subgroups.
- size.buffer: a pass draws at the size of the buffers it writes, and those buffers clear apart from the blit.
- A gbuffers_skybasic or gbuffers_skytextured that writes nothing still hides vanilla's sky or sun and moon, and the world's background is colortex0's clear colour when the pack sets it: vanilla's fog colour was added to Photon's sky, and hid its stars.
Photon v1.4 at 2560x1440 on an M5: 8 to 50 FPS (GPU 125 to 21.5 ms), its sky light from the compute program, quarter-resolution clouds.
- Compute programs (<name>.csh, then _a to _z) are translated and carried before their composite-style program: images (colorimgN in its own format), workGroups and workGroupsRender, shared memory, barriers and subgroups. Compute-only programs are noted as not supported yet. Nothing dispatches them yet.
- size.buffer.<buffer>: a size of its own, relative to the render size (floats) or in pixels (integers).
- shaders.properties and the program switches see the pack's options at their defaults (switches on, value options): Photon sizes its cloud buffers only under #if CLOUDS_TEMPORAL_UPSCALING == 4.
- naga: subgroup operations, memory barriers, and barrier() as a work group barrier.
- The custom textures texture.* names are uploaded and bound to the samplers they name: one texture per dimension of a unit (Photon's 3D scattering LUT under depthtex0), and the lightmap and block atlas by resource location. A sampler with nothing to read gets a stand-in of its own dimension instead of failing its pipeline.
- Outputs past the last buffer RENDERTARGETS names go nowhere, as in GL, instead of an attachment the pass lacks (a crash).
- A shader the driver's compiler refuses (an internal error) skips its program as a validation error does.
Photon v1.4 now loads all 27 overworld programs and draws its lighting, shadows, fog and volumetric clouds.
naga's SPIR-V writer panicked on Photon's deferred4 ("Expression is not cached"): the GLSL frontend passed a pointer into the middle of a variable for f(s.field), which only Metal's writer takes. The naga pin moves to a fix that spills such arguments through a local and writes them back after the call, as GLSL's copy-out says.
real_packs with BACKENDS=1 hands every translated module to the SPIR-V, HLSL, GLSL and MSL writers with wgpu's settings, so this is caught on any machine.
An R32I or R32UI buffer a program binds as colorimgN is made with atomic storage usage, as the pack's own images already are: Metal drops texture atomics on a texture made without it, so imageAtomicMin on colortex9 did nothing.
atomicAdd(sums[i], n) and atomicMax(lights[k].w, w) on a shared ivecN or uvecN are GLSL that naga can't express (it has no atomic vectors): such a vector, or array of vectors, becomes an array of its components, a component an element of it, and the whole vector is read and written through functions of ours. Rethinking Voxels' light lists use them.
A prepare, deferred, composite or shadowcomp program of compute stages only (.csh without .vsh and .fsh) runs its stages in order where the program would draw. shadowcomp programs also run the compute stages they carry.
- IS_IRIS is defined: packs take Iris's extensions (compute programs, images, custom textures) only where it is, and Rethinking Voxels draws an error screen without it. iris.features.optional defines IRIS_FEATURE_<name> for the features Horizon has; a required one it lacks is noted.
- A global initialized with the size on its type (ivec3[6] offsets = ivec3[](...)) kept only the size as its type.
- naga: constant shifts drop the bits shifted out, as GLSL says (1 << 31).
The pack's renderer asks the adapter what it can do instead of assuming Metal or desktop Vulkan. Without compute shaders compute programs are skipped; without storage writes from fragment or vertex shaders the programs that write images or buffers there, and geometry stages, are skipped; an image whose format can't be a storage texture is left out with the programs using it; without clear_texture images are cleared by copying zeros. Each leaves a note, and the bind group layouts made outside the error scope (storage buffers, geometry indices) only name stages the device supports, so none of it can stop the game.
HORIZON_SHADER_PACK_WITHOUT=compute,fragment-storage,vertex-storage,clear tries the fallbacks on a device that has it all.
- shadow.culling=false draws every section within the shadow distance and every face of it; reversed does so within voxelDistance (default 160) and culls beyond as before. Horizon drew only the faces turned to the light, and a pack voxelizing the world in its shadow pass lost the others.
- A geometry stage's image and buffer writes run in one of the invocations that stand for its triangle; in an expression the others read the value instead. The instances drawn follow from the strips EndPrimitive ends.
- at_midBlock for flat terrain: the offset to the centre of the face's block, in 64ths.
- firstPersonCamera.
- naga: imageAtomic* give the texel's value before the operation.
- Storage buffers are zeroed once and kept across frames, bound in set 4 to every program that declares the buffer block.
- The pack's images are made at their size, cleared before each frame when asked, and bound as images by name and as textures by their sampler name.
- Images an atomic function uses bind for atomics; the device asks for texture atomics, storage buffers in vertex shaders and texture clears where the GPU has them.
Rethinking Voxels r0.1-beta9 loads 10 programs and draws; its shadow pass (a geometry stage) and compute-only programs remain.
Packs invert only gbufferProjection's perspective terms when they rebuild positions, so a bob inside it moved every rebuilt position, and the lighting with it, while walking. The bob and the nausea spin are now part of the model-view, where GL drew them; the product is the same, and the hand's projection no longer adds the bob a second time.
gbufferModelViewInverse.1.0 read row 1, column 0: the transpose. Packs turn a view direction to the world as inverse.0.0 * x + inverse.1.0 * y + ..., so Photon's sun direction (from sunPosition) turned with the camera: the sky lit as another time of day when looking around, swung with view bobbing, and the sun was missing.
shaders.properties is preprocessed before its lines join: a value continued with \ just before #else (Photon's moon_phase_brightness) swallowed the directive and the other branch, so the uniform was never defined. Directives now apply while lines join, and lines of branches they turn off are dropped.
Each dimension of a TEXTURE_* entry is at most 16384, and the bytes it needs are counted
without overflowing, so a pack asking for a huge volume gets a note instead of a wrapped
length check.
Photon translated 45 of its 88 composite-style programs and none of its overworld terrain, sky set-up or final pass; now all 88 translate and every dimension loads its full set.
- A struct passed between stages (flat out CloudsParameters p) keeps the pack's declaration as a private global and travels as its members packed into vectors, copied by our main.
- sampler, texture2D and Vulkan GLSL's other texture and sampler type names are free names in desktop GLSL (sampler2D sampler); they are renamed unless called.
- naga's GLSL frontend lacked textureGather, sampled implicit levels in vertex shaders, broke struct constructors with array members, vecN(bool) and vector % scalar: naga is patched with the fixes until a release has them.
- real_packs prints every loader note with SHOW_ERRORS.
texture.<stage>.<name> and texture.noise name the files a pack binds to its samplers: PNG images, and raw 1D, 2D and 3D data with their size, format and type, each with its .mcmeta (blur, clamp). Post carries them, with a note for each that can't be read; nothing draws them yet.
Shader packs need fixes to naga's GLSL frontend that no release has yet. They go on top of this
copy in the commits that need them, as wgpu-hal's fix did, instead of a git dependency.
- A fragment output declared float, vec2 or vec3 (int and uint too) is written as the vec4 of its kind, alpha 1: wgpu's targets take four components.
- inverse() of a mat2, mat3 or mat4 is a function of ours (the adjugate over the determinant): naga writes it neither for Metal nor for WGSL.
- alphaTestRef is a constant of the program: its alpha test's reference.
- Solid and cutout terrain draw together, so terrain gets the cutout test (alpha > 0.1) unless alphaTest.* says otherwise.
- The naga pin moves to the fixes for pointers into vectors passed as out arguments and field selection on a matrix column.
A function-like macro called across lines (Photon's clamp01(\n ... \n)) was left unexpanded. Its lines now join the line the call starts on; the lines after it stay, empty, so line numbers still map back.
The auto-reconnect's `allowed` matched English keywords ("ban", "server
closed", "online mode"...) against the disconnect's resolved text. The
texts arrive resolved in the player's language — a vanilla server's
"Server closed" is `服务器已关闭` to a zh_cn client, so a legitimate retry
was refused, and Horizon's own refusals (NOT_SIGNED_IN, the invalid
address) could only stay English for the matcher to see them.
The disconnect's translation key now travels with the text: the net
thread's reasons carry it (`Reason { text, key }`) from the server's
component's `translate` field, the session-service refusals' inner keys,
and Horizon's own keys (new `horizon.multiplayer.not_signed_in`,
`invalid_address`, `connected_friend_direct/relay` in all six bundles;
the friend connection's status texts and the local world-open progress
line move to keys too). `allowed` decides from a key list first — the
keyword scan stays as the fallback for plain reasons (OS errors,
servers' literal kicks in the server's own language), whose behavior is
unchanged.
NOT_SIGNED_IN is localized now that its classification is key-based, and
`load_language`'s no-language fallback picks Horizon's bundled strings up
so the net thread's keys resolve there too.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
native_entity_join_level(dimension, entity, kind, pos, loaded_from_disk,
canceled) fires as vanilla's PersistentEntitySectionManager.addEntity:
from spawn_item, add_orb, /summon and chunk loads. loaded_from_disk is
true only for an entity that comes back with its chunk; portal and
gateway arrivals and /summon go through the chunk-load queue, so they are
marked there as fresh adds. A cancel takes the entity back out, and a
/summon of it fails with commands.summon.failed.uuid. API 41.
Part of #5162 and #4953.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_block_entity_fluids declares a mod block's tanks, as its container
declares its slots: each tank's capacity, the tanks each face reaches, and
which a fill may reach and a drain may take. The tanks' contents live in
the entity's saved compound under `fluids`, so they survive restarts and
the block's own tick reads them there.
native_fluid_tanks reads them through a side, native_fluid_insert fills
(first into tanks holding the same fluid, then into empty ones) and answers
what did not fit, native_fluid_extract drains and answers what it took.
Both also work a cauldron: it holds thirds of a bucket, a lava one only
whole, and powder snow none, as it is no fluid. Water and lava are the
fluids; a tank holds one at a time.
Co-Authored-By: ZCode AI agent (GLM-5.3)
Review found that the by-id path dropped what `mob_hit` applies: a mob in its
death animation, the mob the player rides and the camera entity are never
clicked by the pick, so naming one in a packet must not reach the feed, shear,
leash or name paths either. `named_mob_pickable` is that rule, shared by both
paths and unit-tested on its own, since the pick's copy cannot be tested
without a world.
Generated with DeepSeek Harness (deepseek-v4.1-flash)
Co-Authored-By: DeepSeek Harness <[email protected]>
ServerGamePacketListenerImpl.handleInteract resolves packet.entityId(), checks
it and the range, and hands that same entity to Player.interactOn. Horizon aimed
the guest at the named entity and then picked one again from the view, so a mob
that moved between the eye and the target took the click, and a block in the way
stopped an interaction the packet had already validated (#4651).
GuestUse::Entity now carries the packet's id and location, and the interact_
helpers use it as it is; the local player still picks from the crosshair, the way
Minecraft.hitResult does.
Generated with DeepSeek Harness (deepseek-v4.1-flash)
Co-Authored-By: DeepSeek Harness <[email protected]>
`slot_position` asks the hopper's and dispenser's stock layout before its
native-screen arm, so a mod screen on a `minecraft:hopper` or
`minecraft:generic_3x3` menu never reached the slots it declares, and its
`inventory_y` was ignored: the mod drew its screen while every slot stayed on
the stock grid (#5141, item from #4953).
Only that layout stands back, and only while a mod screen is shown; the rest of
those menus was already the mod's, since `draw_native_screen` owns the
background and `screen_height` the height. With no mod screen the stock grid
answers exactly as before.
Generated with DeepSeek Harness (deepseek-v4.1-flash)
Co-Authored-By: DeepSeek Harness <[email protected]>
Migrate legacy side files before loading End chunks and retain gateway source chunks while resolving new exits.
Co-Authored-By: Codex GPT-6.1 <[email protected]>
Settings apply at every pointer move of a dragged slider, and since the
Exclusive Fullscreen rows that listed the monitor's video modes each time:
about 28 ms on Windows, so the handle lagged and jumped. The list is now
kept until the window's monitor changes, in game and on the startup menu.
horizon-ctl options-set keeps it too (with_option rebuilt Settings without it).
Fixes#5133
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a shaders/ pack, Iris gives terrain, translucent terrain and fluids their colour without
vanilla's face shading: packs light faces themselves (oldLighting, off unless the pack turns it
on). Horizon baked the shading in, so packs shaded faces twice: a probe pack's colours per face,
Horizon over Iris, were 0.8 north and south, 0.6 east and west, 1.0 up. Sections now mesh
without it while a shaders/ pack runs (Mesher::shader_pack_lighting), and remesh when that
changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A click on a block that opened nothing left use_target set, so a menu an
entity opened next (a chest minecart's generic_9x3) could take the machine's
screen when the machine declared the same menu (arismoko on #5061). Using an
entity now clears it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A jigsaw start (villages, outposts, bastions, ancient cities, trial chambers,
trail ruins) is loaded from its saved Children (PoolPiece::from_saved), as
StructureStart.loadStaticStart does, so the neighbours place the parts that
cross into them and the beardifier sees them. Another structure's pieces
have no loader: it is the generator's own start when that has the same name
and the same pieces' box, else the name and box alone. Decoded once per chunk.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The window's server thread publishes the generators; end.locator is only
set on the deprecated in-frame path, which is not extended.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
DensityCache holds DebugEntryChunkGeneration's lastPos and result; the test pins
the sampling on a new block or dimension, the cleared line without a
generator, and the clear when another world opens.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_chunk_load, with whether the chunk is new, and native_chunk_unload,
including a level that closes for being empty. native_player_logged_in and
native_player_logged_out come in pairs however a player leaves: a disconnect, a
kick, the server stopping or failing. At a stop they're told before the save,
still listed, and a last message goes out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DebugEntryChunkGeneration adds its group only in the server-level branch. With no
generator (a remote server, a LAN world joined, the server thread not there
yet) the cached line of the last world was still drawn; it is cleared
there, and when another world opens.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
smooth() starts from the first value it sees. The load-time dry run that finds unknown names
evaluates every custom uniform with the built-ins at zero, so each smooth() started there:
Photon's biome_temperature (smooth over 1000 s) read -0.97 on the plains where Iris reads 0.6 at
once, and took minutes to get there. Smoothing now restarts after the dry run, and on frames
before the camera's biome is known (its chunk not loaded yet).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
edits_the_lists_while_stopped failed now and then on Linux CI with "a server
is already running in /tmp/horizon-server-lists-<pid>". The folder's lock is
an flock taken with try_lock; while another test in the same binary spawns a
process (tar, id, the engine), the child holds a copy of the lock's
descriptor between its fork and exec, so the lock is briefly taken and
try_lock answers WouldBlock. one_supervisor_per_folder already retried for
that window (4513af899); the lists test did not, neither for its own lock nor
for the edits, which take the lock inside lists::run.
The retry moves into supervisor::retried, a test-only helper (50 tries, 20 ms
apart), used by one_supervisor_per_folder and by the lists test for its lock
and for the edits that must succeed. The edits that must fail while the test
holds the lock are unchanged, and so is lists::run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the active world recipe tables when handling remote recipe placement, and let the server own remote grid and ghost updates instead of predicting them on the client.
Co-Authored-By: Codex <[email protected]>
LZ4BlockInputStream decompresses each block with lz4-java's
LZ4JavaSafeFastDecompressor and calls it corrupted unless the decompressor
reads exactly the block. That decompressor has its own end conditions: a
sequence whose literals leave less than 8 bytes of output, or of input
past them, must be the last and end the output, and an offset of 0 fills
the match with zeros. lz4_flex's decoder disagrees both ways, so Horizon
loaded some streams lz4-java refuses (with whatever they decoded to) and
refused some it loads.
decompress_lz4_java ports that decompressor, fed from a buffer kept as
LZ4BlockInputStream keeps compressedBuffer (it may read past the block,
up to the buffer's end), and decode_lz4 requires it to read the whole
block. lz4_flex still compresses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game's generator is Persistent<VanillaTerrain>, which forwarded
final_writes but not final_block_entities, so the trait default (none)
replaced the chests and spawners of structure pieces reaching into a chunk an
older version saved. Test through Persistent. Also adapts saved starts to
the starts_at wait/try split on main.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
DebugEntryChunkGeneration.display keeps lastPos and result and recomputes
only when the feet BlockPos changes; F3 sampled every frame. The line reads
the pipeline's samplers only, so it now goes through the generator and no
longer takes the shared query worker (no try_lock, no frame without the line).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_container_slots, native_extract_item and native_move_items follow the
rules hoppers use; a move puts a refused item back, so none is lost.
native_dimension names the level for a machine's tick, which isn't told it.
native_insert_item goes one item at a time and tells the container once. It
no longer overfills an empty slot, leaves a client's copy alone, and can't
reach a machine's slots during its own tick, where what went in was lost.
Hoppers change with it, as vanilla has them: a shulker box doesn't go into a
shulker box, a stack's max_stack_size component sets its limit, and comparators
read it. A comparator's fill ratio is clamped to full, where vanilla's
count-at-most-limit invariant keeps it: a stack a mod wrote over its own
limit can't push the signal past 15. A slot whose block entity went away
keeps the item, rather than losing it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: ZCode AI agent (GLM-5.3)
RegionFile.write and clear end in writeHeader, which writes both header
sectors from memory. The constructor has already zeroed the entries it
found invalid there (overlapping the header, empty, past the end of the
file), so a write leaves them zero on disk. Horizon wrote only the
touched slot's offset and timestamp, so stale invalid entries stayed in
the file for other tools to read.
write_header replaces write_header_entry and writes both sectors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod usually has its own itemGroup.<mod id> already (reservedbytes on
#5066): "title" is looked up as a lang key first, English text when it is
none. itemGroup.<mod>.<name> still overrides it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only call `forget_sync_data` if the generated overlay contains files.
This prevents unnecessary data clearing when no file updates are
required for the LAN overlay.
NbtIo.readUnnamedTag skips the root's name with StringTag.skipString
and never decodes it, so a file whose root name isn't modified UTF-8
loads in vanilla. Horizon decoded it and refused the whole file; for
level.dat that meant the world opened with fresh defaults (game mode,
cheats, time) saved over the real ones.
read_named and read_named_streaming read the root name leniently: a
name that isn't modified UTF-8 is read as empty. Every other string,
compound keys included, is still checked, as DataInput.readUTF does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LockCode.canUnlock lets a spectator or a player whose main hand matches the
lock's item predicate open a container; BaseContainerBlockEntity.createMenu
refuses anyone else with an action-bar message naming the container and one
chest-locked sound. Horizon kept the tag on disk and read it nowhere, so a
locked chest opened for anyone, /data get did not show the lock, and
/setblock could not make one.
The lock now lives where the loader already put it, one place reads and
writes it, and the open funnel refuses with vanilla's message and sound.
Lightning, additive quads and translucent particles draw in the OIT stages
there, and only the OIT entities got a mark after them. The firework mark
also takes the same base as the draw it follows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NbtIo.write goes through StringFallbackDataOutput: a writeUTF that would
exceed 65535 encoded bytes logs "Failed to write NBT String" and writes
an empty string, and the rest of the tree is written. Horizon's
write_named failed instead, and since a save batch's records are encoded
before any is written, one block entity with such a string (a long
command block command, a book) made the whole batch fail: every chunk
changed since the last save was lost, not only that one.
The disk writer (write_named) now writes such a string empty; the
network writer still refuses it, as vanilla's does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.getRootVehicle follows getVehicle up through any vehicle, and
PlayerList.remove removes getPassengersAndSelf of that root, the same
entities Entity.save wrote under Passengers for
ServerPlayer.saveParentVehicle. The ride saved a minecart without its mob
passengers while remove_vehicle_tree removed them, so they left the world
unsaved, and vehicle_root climbed a mob stack to a boat but not to a cart.
- vehicle_of: the mob, boat or minecart a mob rides; parent_vehicle_root
climbs it to the root, so a stack in a cart has the cart as root.
- vehicle_tree_nbt puts the cart's mob passengers under Passengers, as
for a boat (add_passengers, out of put_mob_passengers).
- remove_vehicle_tree removes vehicle_tree over the living mobs, the ones
put_mob_passengers saves.
- Unit tests over the pure parts: a lone rider saves the horse, two
players on one boat save nothing, a mob in a boat rides under it, a
cart saves and takes its mobs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ensure `forget_sync_data` is called unconditionally when packing files
for LAN overlays and when disconnecting from a remote session. This
prevents stale resource synchronization data from persisting across
different connection states.
The window's world has no mod containers (the server tick installs them), so
mod_menu_at was always none there and no mod screen opened. Ask
native_block_entity_container for the clicked block instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A world with the `trade_rebalance` feature flag now plays the flag's
built-in pack: the overridden villager trade tags and trades, the
librarian enchantment trades and the changed chest loot, as vanilla's
server does. `MinecraftServer.configurePackRepository` force-enables a
feature pack whose requested features the world enables
(`enableForcedFeaturePacks`) and drops a selected pack asking for
missing ones: `datapack::enabled_ids` now takes the world's
`enabled_features` and does both, so the pack's files reach the world's
content overlay (`/datapack list` shows it as enabled).
The content tables then merge the pack over the resource root:
`Trades::load` takes the packs' `villager_trade`/`trade_set` files and
their tags (`replace` clears, `TagLoader`), and `Enchantments::load_with`
takes the packs' enchantment tags, so the librarian trades'
`#minecraft:trades/<biome_common>` pools resolve. Along the way:
- `PackOverlay::tag_kind` stripped a leading `:` that is not part of
the `tags/<ns>:<registry>/<path>` key shape, so it never matched and
every pack tag overlay silently no-opped; loot tables' tag overlays
were affected too.
- `/loot give ... loot <table>` rolled the resource root's table when
the table had a random sequence (`self.res.items()`), not the world's
own — the world's tables are used for the sequence branch now.
- The world's flags feed the worldgen overrides and the biome table
(`packed_biomes`, `level_shapes`, the preload reads them from the
world's level.dat), and the integrated open path passes them through.
- Fixes the stacked world-save commit leaving `integrated.rs` calling
`packed_biomes` without its `resources` argument (the bin did not
compile).
Vanilla reference: `MinecraftServer.configurePackRepository`,
`enableForcedFeaturePacks`, `TagLoader.load`,
`SimpleJsonResourceReloadListener`, the built-in
`data/minecraft/datapacks/trade_rebalance` pack.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
The first try (key == target) never held for a mod block: `holds` knows only
vanilla block entities, so every mod screen stayed shut. The screen now opens
when the menu is the one the clicked block's container declares
(`mod_menu_at`), which a chest menu from a command over a stale target is not.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RideCommand.mount took only a mob as the vehicle. A player or a mob now
mounts a boat with a free seat, and a player any minecart no other player
is in, through the same state as a click. /ride <player> dismount goes
through stop_riding, and Entity.getVehicle names a minecart as one.
Part of #4968.
The overlay's key hashed each enabled zip in full, on the assumption that one
file reads in a moment: with a 9 GB pack that read 9 GB at every launch.
A zip over 250 MiB, vanilla's limit on a server pack, is now keyed by its
length, modified time and central directory. Smaller ones keep their bytes, so
a server pack's touched time still doesn't rebuild the overlay, and an edit
that a zip's empty checksums wouldn't show is still seen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
- A mod screen opens only when the clicked block holds the menu: use_target
outlives its use, so a menu opened later (a command's, another mod's) got the
last clicked machine's screen, and its clicks and tooltips (arismoko on #4106
and #4289).
- native_screen_tooltip runs over an empty slot, as its doc says, so a mod can
say what an input takes (arismoko on #4289).
- A mod block whose template has no block entity gets none, with a warning:
breaking it never removed the block entity, which stayed in the save
(reservedbytes and DUzzL on #3996).
- A GUI texture that doesn't decode is named in the log, not skipped silently
(reservedbytes on #4106).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From reservedbytes' test on #4331: a block's item was added to search twice
(blocks' items are already among the plan's items); the page buttons stretched
the 200-wide button sprite into 20 px and lost their sides (now blit_sprite, as
vanilla's Button draws); turning the page while on search, saved hotbars or the
inventory keeps that tab; a mod tab's title comes from itemGroup.<mod>.<name>
in its lang files, the file's title being the English fallback.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content mod declares tabs in data/<id>/horizon/creative_tab/*.json: a
title, an icon and its items. They come after vanilla's, ten to a page
(the top row's first five columns, then the bottom row's), with < and >
buttons above the screen; search, saved hotbars and the inventory show on
every page. Its new items join the search tab under their English names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
"thickness" (2 to 14 pixels) gives a connecting block its own outline and
collision, built as PipeBlock builds its shapes (pipe_shape, checked against
the chorus plant's 64 states). The shapes go through the same per-state path
as a block's "shapes", so a block sets one or the other.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A block declared with template minecraft:chorus_plant and connects_to: a
block tag is vanilla's PipeBlock without the chorus plant's rules: no
support needed, and each of its six sides joins the same block or a block
in the tag, on placement and on every neighbour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Listing the packs opened every zip in resourcepacks/ twice, once for its
pack.mcmeta and once for its pack.png, and each open read the whole file
into memory on the render thread. With 22 GB of packs in the folder, enabled
or not, opening the options screen froze the game for five to seven minutes.
A Zip now keeps the file open and holds only the central directory on, with
the END record normalized as before, the way Java's ZipFile works. Entries
are read at their positions, so clones and threads don't share a cursor.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
flush_chat_queue now goes through accept_chat, as ChatListener.flushQueue runs each handler.
Without it a flushed signed message never reached the last-seen tracker, and vanilla's
server disconnected the client at its next message (checksum mismatch). The harness now
sets Chat Delay, holds a message back and flushes it. send_command parses over chat_tree.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntitySelector.findEntities with a UUID takes ServerLevel.getEntity,
whatever the entity is. target_with_uuid kept only mobs, so /tp, /kill
and /data get entity with a display's, armor stand's or item's UUID
found nothing; the scoreboard's holder lookup kept mobs and items. Both
now go through level_entity.
Fixes#4966
`DataPackCommand` gates feature packs on the world's flags: a pack whose
requested features are not enabled is not available, cannot be enabled
("required flags are not enabled in this world"), and a feature pack can
never be disabled — the flag stays on for the world's life. Horizon
refused every enable and allowed every disable regardless. The command
now reads the world's `enabled_features`: with the flag on, a feature
pack enables and its data loads; without it, it stays hidden and the
refusal names the missing flag.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Vanilla's server save runs `PlayerList.saveAll`: every connected player
is saved to `playerdata/<their own uuid>.dat` — there is no world-owned
player file. The integrated server's save now does the same for its
placed guests (`playerdata::save`), on every save and when a player
leaves, and the owner joins through `PlayerDataStorage.load` on their
own uuid first — the world's own player state is only the fallback for a
first join. Previously the owner's state was stored into the level and
then dropped (`write_without_player`), so position and inventory were
lost on every reload, and the stale single-player file shadowed the
newest-file fallback on load.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
The integrated server's owner plays as the world's own player
(`Owner`): on leave their state is stored into the host's `Level`
(`playerdata::store`), but the server saves with `write_without_player`
(it runs as `dedicated`) and the window's own save is skipped while it
is connected, so the state never reached a file and position and
inventory were lost on every reload. The integrated role now writes the
world's own player file (`players/data/<singleplayer_uuid>.dat`) with
the level, as single player does (`PlayerDataStorage.save`); a real
dedicated server keeps saving its players on their own.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Review of the port against the decompile found three deviations that
stalled carts at corners and broke climbing:
- `stepAlongTrack` ends with `setDeltaMovement(movement)` — the movement
turns onto the exit's direction at the same speed. Without it the
velocity never turned, so a corner's exit selection kept the old
direction and the cart stopped.
- `TrackIteration`'s slope, halt and boost speeds apply once per tick
and carry their flags across the iteration; the port re-applied them
on every rail.
- the player-input push does not replace the natural slowdown and the
speed cap (vanilla applies both), and a stuck step
(did-not-move-but-would-have) zeroes the movement.
Also `isDecending` reads the step's movement (the exit's direction),
not the stale velocity, for the hill's Y projection.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
`AbstractMinecart` chooses its behavior by the world's features: with
`minecart_improvements` the new behavior runs — the slide on slopes
grows with the speed (`max(0.0078125, h * 0.02)`) so a descending cart
keeps its pace, the empty cart slows half as fast (0.975 over 0.96),
the speed cap is a vector normalization instead of a per-axis clamp,
a powered rail boosts by `+0.06` along the movement or pushes a resting
cart 0.2 away from the signal's side, an iteration carries leftover
distance across rails, a hill projects the Y onto the slope, and a V of
opposing slopes rests a slow cart (`restAtVShape`). Without the flag
the old physics runs as before.
Stacked on #4975 (← #4957 ← #4948 ← #4619 ← #4577), whose
`enabled_features` the gate reads.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Vanilla reads the world generation data behind a loading screen before
the Create New World screen opens, again after its Data Packs or
Experiments screen applied a selection, and warns before applying a
selection that asks for experimental features. Horizon showed none of
that.
- `createWorld.preparing` (`GenericMessageScreen`: the menu background
and the centred message on a black box with a 1-pixel border and 12
pixels of padding, no progress): Create New World reads
`vanillagen::shared_data` on a worker while the title shows it, and
opens the create screen when the read is done; the Data Packs screen's
Done and the Experiments screen's Done re-run the read the same way
(`applyNewPackConfig`).
- `ConfirmExperimentalFeaturesScreen`: when the Data Packs screen's Done
picks a feature pack, the warning comes up first — the title, the red
message wrapped to 310 and centred, a 100-wide Details button, Proceed
and Back in the footer, the whole block centred, Escape declining. Its
`DetailsScreen` lists the picked packs bold with the flags they ask
for (`selectWorld.experimental.details.entry`). Proceed applies the
selection, Back reopens the Data Packs screen on the selection it was
opened with (`tryApplyNewDataPacks`'s `onAbort`).
Stacked on #4619, whose Experiments screen and feature-pack plumbing the
confirm screen builds on.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Vanilla's level.dat carries `WorldDataConfiguration`'s `enabled_features`
flat in the Data compound (`FeatureFlags.REGISTRY`'s codec: a list of the
whole set's namespaced ids, `minecraft:vanilla` included, the tag omitted
when the flags are vanilla's default) — Horizon wrote nothing, so a world
made with experiments told vanilla nothing and forgot its flags itself.
`Level.enabled_features` is written with the selection (the flags of the
picked feature packs over vanilla) and read back from level.dat, so a
vanilla-made world keeps its flags too. The behaviors behind the three
flags are still separate work; this is the data they will gate on.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Review fixes on #4619:
- The More tab's `options.press` guard caught button 30 before the
Experiments arm could, so the button did nothing; the guard lets 30
through now.
- Create World's `open_world` overwrote the `datapacks.json`
`datapack::install` had just written, losing the unselected world
packs' `disabled` entries, so a pack the player left off came on. The
write now only happens when `install` did not run (only Experiments
used), and it creates the world folder and logs failures instead of
dropping the error.
- `level.dat`'s `DataPacks.Disabled` also lists the world packs the Data
Packs screen held back, as vanilla's `DataPackConfig` does.
- The completion registries' pack list gains the feature packs, so a
pack enabled through Experiments completes its worldgen ids.
- The feature packs on the Data Packs screen compared against the
resource pack format and drew red; they compare against the data pack
format now.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Vanilla's More tab offers Game Rules, Experiments and Data Packs; the
Experiments button was drawn disabled ("Experiments have no screen yet").
It now opens vanilla's ExperimentsScreen (`title/experiments.rs`): every
feature pack the resources root ships under `data/minecraft/datapacks`
(`ServerPacksSource`, `PackSource.FEATURE`), each with a 44-wide on/off
switch at the right of its row, its description grey underneath, the red
warning wrapped to 310 and centred, Done and Cancel in the footer —
vanilla's `HeaderAndFooterLayout` + `SwitchGrid` geometry. Switching a
pack adds or removes its id in the world's data pack selection, which the
Data Packs screen and the world's DataPacks then see; Done keeps the
other packs and rejoins the picked feature packs last, reversed
(`onDone`), Cancel and Escape leave the selection as it was.
The feature packs also join the world's repository, so a picked pack
reaches the game:
- `datapack::feature_packs` scans the resources root's feature packs and
`screen_packs` offers them on the Data Packs screen (available, off),
as `PackSelectionScreen` does;
- a world remembers its selection in `datapacks.json` (the pack stack's
own file) and `level.dat`'s `DataPacks` (`DataPackConfig`);
- `pack_stack`, `packed_biomes` and `world_worldgen_overrides` resolve
the ids against the resources root, and a feature pack stays off
unless the world enabled it (vanilla starts `PackSource.FEATURE`
unselected), so existing worlds behave exactly as before;
- `/datapack` shows an enabled feature pack with the feature source and
can disable it again, while `enable` still refuses one
(`commands.datapack.enable.failed.no_flags`) — the three flags'
behaviors are not implemented yet, but the packs' data files
(trade_rebalance's loot tables and tags) reach the content tables
through the pack stack.
Dropping a pack on the window (`PackSelectionScreen.onFilesDrop`): a
zip, a pack folder, or a folder holding exactly one pack (`PackDetector`)
is copied into the create screen's pack folder, where the Data Packs
screen's existing watcher lists it within a second; anything else is
refused and logged.
Stacked on #4577, whose unified pack list the resolution goes through.
Verified headless: a world with `trade_rebalance` in `datapacks.json`
lists it enabled ("[trade_rebalance (feature)]"), disables it with
`/datapack disable`, and the other feature packs stay available.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
Co-Authored-By: ZCode Agent <[email protected]>
Review fixes on #4577:
- The window's Game on the integrated-server path kept `world_biomes`
unset: the chunks it draws carry the packs' biome ids past the built-in
list, but nothing named them. `open_integrated` builds the world's
table now, as `finish_open_world` does for the load path.
- The integrated path's own encoders read `res.biomes()` past the world's
table: `lan_host`'s chunk encoder and `/execute if biome`'s naming go
through `world_biomes!` now, so a packed biome keeps its name and its
place on the wire (a guest still needs the table sent over the
connection; a remote server join clears the last world's table so no
stale names linger).
- A tag in a sub-directory of its registry never matched: `content_id`
attached the namespace to the file's last segment
(`tags/worldgen/biome/has_structure/minecraft:village_plains`), while
the lookups build `tags/{registry}/{ns}:{path}`. The store key is the
tag id now (`tags/worldgen/biome/test:sub/mytag`), `PackOverlay`
gained `tag_kind` for the registry walks, and the readers match.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
`WorldgenOverrides` and `Biomes::with_packs` both take the world's packs,
but the client built the biome table and the worldgen from different
lists: `content_overlay` reads `datapacks.json` (empty until a world
writes it) while the world's worldgen uses `datapack::discover` with
`enabled_ids`. A pack's biomes therefore never reached the table
`WorldgenOverrides::finish` tests against, so it dropped every new biome
("cannot add biomes yet") and the generator then failed on a material
rule naming one ("unknown material biome").
`world_loading::packed_biomes` builds the table from the same list the
worldgen is built from, and the completion registries read the same
overridden ids instead of `DataStore`'s files, so `/locate biome` accepts
and completes a packed biome.
Verified with Terralith 2.6.5 as the world's only pack: the world opens,
`/locate biome terralith:alpine_highlands` finds it, and `/locate biome
#c:is_lush` reports `terralith:cave/underground_jungle`.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`Biomes::with_packs` (the built-in table with a world's pack files over
it, as `RegistryDataLoader` layers registries) had no callers, so a
packed biome had no client data and `Biomes::id` resolved it to plains —
every packed biome rendered, spawned and saved as the fallback.
The table is built when a world opens, from the same pack overlay the
content tables use, and every biome lookup goes through it
(`world_biomes!`, a field-level borrow for the loops that also take
`&mut self`). `world_loading::make_streaming` builds the same table for
the generator, so the ids the terrain names and the ids the client draws
agree.
A world with more than 128 biomes needs the palette's 8 bits, which the
earlier commit gives it.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`command-registries.json` lists the game's own ids, so `/locate biome`
refused a packed biome before searching and offered no packed biome or
tag in its completions. The packs' `worldgen/biome`, `structure`,
`structure_set`, `template_pool` and `processor_list` ids now join those
registries, and so do their worldgen tag ids.
The tag lookup behind `#tag` (`/execute if biome`, `/locate biome #tag`)
read only `data/<ns>/tags/worldgen/biome` from the resource root; it now
reads the world's packs too, in resource-stack order, with a `replace`
file dropping what the lower sources put in (`TagLoader.load`). That also
makes a pack's structure tags work.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`Strategy::biomes(7)` tops out at 128 biome ids; vanilla ships 67 and a
pack like Terralith adds a hundred more (`Biomes::with_packs`), which the
global palette's 8 bits hold.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The sky reads inverse_view * ray, so its mirror is F * inverse_view; the column operation used for view_proj * F left a pitched camera's sky wrong: looking down 40 degrees mirrored a sky looking down. A test compares the rays.
A scan for the water plane reads up to 60 000 blocks on the game thread; it now runs when the camera moves 8 blocks or every 5 seconds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
A normal within 0.99 of an axis (about 8 degrees) snapped to it, which flattened flowing water's gentlest slope (about 6 degrees). The snap now reaches about 3.6 degrees, still enough for distant block faces, and the doc says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
NativeFrame::write skips title frames, so the first world frame after a title screen, a pause or a world load reprojected from a stale camera with a delta_time of seconds. Past half a second the frame counts as the first. The easing is a pure function, tested.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBqm1v5zTsDGwuZF6XKFjL
Like a server's streaming, a headless game's doesn't mesh sections it can't draw. Measured
on a single-player world after loading: 0 sections meshed instead of 21287, 853 MB resident
instead of 1117 MB, and about 15% of a core in steady state instead of 20%.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main reached NATIVE_HOOKS_API 36. native_visuals and native_emissive
(API 8 and 9 on this branch) now share API 37, in the module doc and
docs/native-mods.md. The emissive layer moves to the terrain shader's
@location(9), after main's fade, local and visibility.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Refactor the pause screen button layout to correctly handle the
`pause_screen_additions` tag and the `minecraft:server_links` fallback.
The new implementation uses a helper function `pause_grid` to calculate
the grid origin and ensures that the custom additions button (ID 14)
is rendered as a full-width row, properly pushing down subsequent
menu options.
Additionally, implement `Minecraft.getQuickActionsDialog` to support
opening the quick actions dialog via the `key.quickActions` binding.
Update port maps to reflect the new implementation details for
`PauseScreen` and `Minecraft` quick actions.
A DialogScreen is a screen in vanilla, so KeyMapping.releaseAll runs.
Horizon kept Screen::Playing, dropped key and right-button releases, and
left W and use held after the dialog closed.
`horizon-client --headless` (`horizon-ctl launch --headless`) runs where no display server
is: winit's event loop needs one even for --offscreen, so the game runs its own loop
(game/headless.rs) through the EventLoopCtl handle, one frame a tick. Each step is
Game::idle, which runs the frame through the RedrawRequested path with WindowId::dummy();
handle_window_event takes that id when there is no window. No renderer is made and audio
is silent; the loading screen is skipped (synchronous startup). It counts as offscreen for
everything else. Quitting saves the world as a window's game does (Game::shut_down, now
shared with winit's `exiting`).
Measured on a Ryzen 7 3700X with no DISPLAY: a single-player world opens through the
server thread, the control socket moves, commands and menus work, no GPU device is opened,
about 1.1 GB resident, and in steady state the client's own thread uses 1.6% of a core.
Screenshots and clicks at GUI points answer "no renderer yet" until vision (#4951).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Track active animated sprite layers per section and cull animation ticks and GPU uploads for sprites not present in visible or shadow sections.
- Mask OPAQUE_LAYER (bit 15) when collecting animated layers from mesh quads so opaque animated blocks (magma, lava, prismarine, etc.) are animated.
- Include active animated layers from visible LOD tiles (and any fading LOD tiles).
- Unit tests for inactive sprite culling and opaque animated face layer collection.
Co-Authored-By: Antigravity <[email protected]>
An update that stopped between copying the new binary and moving it into
place left <name>.update-<pid> in the install folder, and the start-up
clean-up only matched .old- files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A headless client (#4951) has no display, so it cannot run winit's event loop. The game's
methods used winit's ActiveEventLoop only to stop the loop and to say when to wake next:
they now take `&dyn EventLoopCtl` (game/event_loop.rs), which winit's loop implements.
window_event and about_to_wait keep their bodies, now the inherent handle_window_event and
idle, and the ApplicationHandler methods forward to them. The control socket's and the pad
menu's synthetic input call handle_window_event. Nothing changes for a window's game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4231 (on main) already returns the replaced piece in creative and
discards it there when the inventory is full. What this branch still
adds: survival throws the piece that no longer fits like Player.drop
(stack, false) instead of spawning it motionless at the eyes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main split open_device into open_with_retry and open_backend (#4064). The
CPU-adapter check now sits right after request_adapter and fails the
backend, so the non-conformant retry doesn't pick the same adapter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MushroomCow.get answers with minecraft:mooshroom/variant, red or brown, and
applyImplicitComponents reads it back, so a mob spawned from an egg keeps
its colour. finalize's species_variant_component listed wolf, cat, frog,
pig, cow, chicken and zombie nautilus but not the mooshroom: the component
was never reported, variant_name fell through to the frog's table and
variant_index could not recognise "brown".
The save format is untouched: a mooshroom still stores Type, not variant.
The ticks == 0 guard never let a dropped item reach ItemEntity::tick, the
only place ticks counts up, so every drop froze: no gravity, pickup delay,
merging or despawn. An item now waits one tick only when its id is past
the last one the tick saw after its functions ran.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's OIT_ENTITY shares the layer's high bits with the HDR tier: the overlay
consumers mask 0xffff, vs_lightning masks the tier, and a test pins that a tier
keeps the material.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- options_ui: Space/Enter on FocusId::Cycle now emits Event::ButtonClick
before cycling (was silent before)
- options_ui: wheel on a keyboard-focused slider steps it; wheel on a
keyboard-focused cycle button cycles it (Horizon improvement; Vanilla 26.3
does not intercept the wheel for sliders at all)
- graphics_ui: same wheel improvement for the graphics settings page — when
can_change_value is set, wheel steps the focused slider or cycles the focused
cycle item instead of scrolling the list
- title.rs: confirm_key_size returns (Action, bool); the bool is true for any
enabled focused button press, false for disabled buttons or no-ops
- game.rs: title keyboard confirm plays the click sound from the bool, not just
on Action::Play
- startup_menu.rs: extract .0 from confirm_key_size return
- Tests: options_keyboard_space_enter_emits_button_click_for_cycle_and_default
and title_screen_keyboard_navigation_and_activation updated and passing
Co-Authored-By: Antigravity <[email protected]>
Review fixes on #4577:
- The window's Game on the integrated-server path kept `world_biomes`
unset: the chunks it draws carry the packs' biome ids past the built-in
list, but nothing named them. `open_integrated` builds the world's
table now, as `finish_open_world` does for the load path.
- The integrated path's own encoders read `res.biomes()` past the world's
table: `lan_host`'s chunk encoder and `/execute if biome`'s naming go
through `world_biomes!` now, so a packed biome keeps its name and its
place on the wire (a guest still needs the table sent over the
connection; a remote server join clears the last world's table so no
stale names linger).
- A tag in a sub-directory of its registry never matched: `content_id`
attached the namespace to the file's last segment
(`tags/worldgen/biome/has_structure/minecraft:village_plains`), while
the lookups build `tags/{registry}/{ns}:{path}`. The store key is the
tag id now (`tags/worldgen/biome/test:sub/mytag`), `PackOverlay`
gained `tag_kind` for the registry walks, and the readers match.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
`WorldgenOverrides` and `Biomes::with_packs` both take the world's packs,
but the client built the biome table and the worldgen from different
lists: `content_overlay` reads `datapacks.json` (empty until a world
writes it) while the world's worldgen uses `datapack::discover` with
`enabled_ids`. A pack's biomes therefore never reached the table
`WorldgenOverrides::finish` tests against, so it dropped every new biome
("cannot add biomes yet") and the generator then failed on a material
rule naming one ("unknown material biome").
`world_loading::packed_biomes` builds the table from the same list the
worldgen is built from, and the completion registries read the same
overridden ids instead of `DataStore`'s files, so `/locate biome` accepts
and completes a packed biome.
Verified with Terralith 2.6.5 as the world's only pack: the world opens,
`/locate biome terralith:alpine_highlands` finds it, and `/locate biome
#c:is_lush` reports `terralith:cave/underground_jungle`.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`Biomes::with_packs` (the built-in table with a world's pack files over
it, as `RegistryDataLoader` layers registries) had no callers, so a
packed biome had no client data and `Biomes::id` resolved it to plains —
every packed biome rendered, spawned and saved as the fallback.
The table is built when a world opens, from the same pack overlay the
content tables use, and every biome lookup goes through it
(`world_biomes!`, a field-level borrow for the loops that also take
`&mut self`). `world_loading::make_streaming` builds the same table for
the generator, so the ids the terrain names and the ids the client draws
agree.
A world with more than 128 biomes needs the palette's 8 bits, which the
earlier commit gives it.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`command-registries.json` lists the game's own ids, so `/locate biome`
refused a packed biome before searching and offered no packed biome or
tag in its completions. The packs' `worldgen/biome`, `structure`,
`structure_set`, `template_pool` and `processor_list` ids now join those
registries, and so do their worldgen tag ids.
The tag lookup behind `#tag` (`/execute if biome`, `/locate biome #tag`)
read only `data/<ns>/tags/worldgen/biome` from the resource root; it now
reads the world's packs too, in resource-stack order, with a `replace`
file dropping what the lower sources put in (`TagLoader.load`). That also
makes a pack's structure tags work.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`Strategy::biomes(7)` tops out at 128 biome ids; vanilla ships 67 and a
pack like Terralith adds a hundred more (`Biomes::with_packs`), which the
global palette's 8 bits hold.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Return first valid dialog's tag instead of Tag::List (show_dialog can't parse List)
- This fixes pause_screen_additions button actually opening the dialog
Predict craftable PlaceRecipe placements on the remote client, look up
ghost recipes by display id, and clear the ghost when the server updates
the open menu so red ghost slots do not stick after a successful place.
Fixes#4549
Co-Authored-By: Composer <[email protected]>
Co-authored-by: Cursor <[email protected]>
Judged in tools/hdr_gallery.py's gallery under the tuning standard. Flames, lava,
crying obsidian, the froglights, the jack o'lantern and the larger amethyst shine all
over; brewing stand, enchanting table and the smaller buds go bright; candle cakes and
the ender chest don't shine (the cake's frosting would in daylight). The bright-parts
mask goes to 0.32 of the way from a sprite's darkest pixel to its brightest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tier windows judge a pixel by a fixed brightness, tuned on vanilla's
textures: a resource pack with a lighter torch handle lifted the handle
too. When HDR output turns on, a small pass now measures every block
sprite's darkest and brightest opaque pixel (gfx/hdr_stats.rs), and
terrain marks a light source's pixel only when it is as bright as its
sprite's threshold, set between the two: for most light sources near
their brightest (their bright parts shine), for those marked WHOLE at
their darkest (they shine all over). The tier windows still decide how
much. Nothing is measured with HDR off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Restore Bug Report and Feedback icons with click handlers (open confirm_trusted_link)
- Fix dialog leak between worlds: clear sync data on world close (forget_sync_data)
- pause_screen_additions: match vanilla behavior (single button opening dialog_list)
- Fix clippy warnings: use ? operator with as_ref()
- Fix tests: update pause_buttons/pause_icons calls for new signature
Completing a subtree used to mark every active descendant dirty, so the
final chunk of a cold view remeshed the whole view. A completion that does
not change a tile's own drawn state now touches no mesh.
Cutout publication waited for every mesh job in flight, so continuous
loading could defer it indefinitely. It now tracks the active tiles whose
meshes must stop clipping the changed columns; a retired waiter hands its
wait to its active descendants, and later batches do not delay it.
An active tile index (`unmeshed`) replaces the all-active scan in removal
gating, dispatches coalesce while the pool is saturated, and the neighbours
behind a split tile's shared face are no longer remeshed. A saved summary
without completed detail no longer releases a covered empty stand-in.
Co-Authored-By: omp (DeepSeek V4.1 Flash) <[email protected]>
handleLogin prepares the key pair only when the login packet says the server
is in online mode, and tick asks shouldRefreshKeyPair only once there is a
chat session. sendCommand sends ServerboundChatCommandSignedPacket whenever
the command has signable arguments, with no signatures when there is no
session (ArgumentSignatures.signCommand drops null signatures).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Two decorations reaching the same chunk an older version saved ran on their own
snapshots and the last write won; a chunk's features each see what the
one before placed. The proto chunks already exclude each other by being
checked out: the final chunks a running decoration writes into now do too.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Completing a subtree used to mark every active descendant dirty, so the
final chunk of a cold view remeshed the whole view. A completion that does
not change a tile's own drawn state now touches no mesh.
Cutout publication waited for every mesh job in flight, so continuous
loading could defer it indefinitely. It now tracks the sequence of the batch
that replaces the affected tiles; later batches do not delay it.
An active tile index (`unmeshed`) replaces the all-active scan in removal
gating, dispatches coalesce while the pool is saturated, and the neighbours
behind a split tile's shared face are no longer remeshed. A saved summary
without completed detail no longer releases a covered empty stand-in.
Co-Authored-By: omp (DeepSeek V4.1 Flash) <[email protected]>
starts_at returned nothing for a chunk an older version saved, so the
explored part of an upgraded world had no structures to find. It now
returns the starts in the chunk's structures.starts (name and bounding box,
no pieces: they are not rebuilt).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A chest or spawner of a structure piece reaching into a chunk an older
version saved had only its block copied; the generated block entity, with
its rolled contents, now goes with it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
region_bits computed outside the lock with no in-flight guard, so every
worker that asked for a region meanwhile decoded its chunks again.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The F3 overlay is built on the window thread every frame; locking the
shared query worker froze the frame behind /locate biome or /place jigsaw.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The far-terrain scheduler evaluated the whole quadtree on the frame thread
for every finished sample, cloned every active tile's columns into every
mesh job, and handed a whole selection batch to one worker.
Selection is now incremental: a finished sample re-evaluates only its
ancestors (O(tree depth)) and marks only the tiles whose faces or clipping
actually change. The sample queue is a nearest-first priority heap rebuilt
when the view moves a chunk, not scanned and sorted every frame. Mesh jobs
carry one immutable snapshot and borrow columns by reference instead of
cloning them; a pool of workers meshes tiles in parallel with a priority
queue and revision checks. Bounds are tracked while emitting vertices on the
worker, so the frame thread no longer walks every vertex of every uploaded
tile. Removals wait until every overlapping replacement has its mesh, so
fade-out cannot open a hole, and both worker pools are stopped under their
own wait locks.
Selection stays an exact match for Plan::select after every sample, across
camera moves and coverage changes (test); edge-neighbour dirtying, active
descendant redraws and removal gating have their own tests. Column spans are
inline (SmallVec), removing almost all per-column allocations on the
sampling workers.
Co-Authored-By: omp (DeepSeek V4.1 Flash) <[email protected]>
- Re-add clippy::clone_on_copy allow for UniformLocation in command.rs.
- Format indirect_execution expression in adapter.rs to satisfy rustfmt.
- Note in gfx.rs comment that multi_draw requires indirect first instance
support (OpenGL 4.2 + GL_ARB_shader_draw_parameters) in addition to 4.0 indirect draws.
Co-Authored-By: Antigravity <[email protected]>
Forgetting the only profile on the last page left the page empty until
Previous was pressed: the page is now kept to the pages the list has.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- When clicking a pause_screen_additions button, first close the pause screen (return to Playing + capture mouse)
- Then show the dialog
- This matches vanilla behavior where pause menu closes when opening a dialog from it
- Fix button Y position: pause_screen_additions buttons now appear at row 3 (y0 + 122),
above Options/World Options, matching vanilla behavior
- Fix click handler to use correct Y position for additions buttons
- Update pause_icons to account for additions height
- Button label uses external_title (falls back to title if not set)
- Add pause_screen_additions() to read dialogs from the minecraft:pause_screen_additions tag
- Display dialogs with external_title and pause:true as buttons in the pause menu
- Clicking these buttons opens the corresponding dialog
fix: clear held keys when opening a dialog to prevent stuck movement
The client uses #4214's SignatureCache and its handling of a bad packed id, instead of its
own cache. markMessageAsProcessed runs where the chat takes a message, past any Chat Delay,
with whether it was shown; handleDeleteChat now calls LastSeenTracker::ignore_pending.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- AbstractSliderButton / OptionInstanceSliderButton parity:
- ArrowLeft / ArrowRight adjust slider values when in can_change_value mode
- Space / Enter toggles can_change_value focus state
- Escape cancels can_change_value before closing the options screen
- Mouse wheel scrolling over a slider steps its value
- Render state parity: track highlighted only when focused and !can_change_value,
handle highlighted when hovered or can_change_value
- Dialog sliders: support ArrowLeft / ArrowRight nudge and mouse wheel scrolling
- General UI keyboard navigation: Tab/Shift-Tab cycle and 2D spatial Arrow navigation
Co-Authored-By: Antigravity <[email protected]>
OldMinecartBehavior.pushAndPickupEntities also picks up living entities
in the cart's box, 0.2 wider on each side. Horizon only pushed other
carts, so a cow shoved into a minecart stayed on the ground. Eligible
mobs now startRiding an empty cart on contact; players still click to
board, iron golems still push, and a cart that already has a rider
pushes instead. Entity.canRide refuses a warden and a boarding cooldown.
sync_riders treated only boats and mobs as vehicles, so a boarded cart
would drop its rider the next tick. Riders sit on the cart's passenger
attachment, get out when it breaks, and are saved under Passengers.
Vanilla: OldMinecartBehavior.pushAndPickupEntities, Entity.startRiding,
AbstractMinecart passenger attachment, Entity.positionRider.
Co-Authored-By: OhMyAgent (grok-4.6) <[email protected]>
ServerPlayer.saveParentVehicle writes RootVehicle {Attach: UUID of the
immediate mount, Entity: the root vehicle saved with its passengers},
only while the root has exactly one player passenger
(Entity.hasExactlyOnePlayerPassenger). PlayerList.remove then, for such
a player, stops riding and removes the root and its passengers from the
world (UNLOADED_WITH_PLAYER), so the ride leaves with the player and
ServerPlayer.loadAndSpawnParentVehicle spawns it again on the next login.
PlayerList.removeAll does this for each player in turn on shutdown.
Horizon only wrote Attach and left the vehicle in the world, so a vanilla
reader got no vehicle back and the horse or boat stayed behind.
- persist.rs: vehicle_root, vehicle_tree_nbt (mob stack, boat with its
mobs, minecart), parent_vehicle_tag with the one-player check, and
remove_vehicle_tree.
- lan.rs: take_parent_vehicle on a guest leaving (HostEvent::Left) and
save_leaving_guests for lan_close, stop_integrated and stop_dedicated;
the tree goes in the guest's saved compound, the guest unrides.
store_owner writes the full RootVehicle for the integrated owner.
- playerdata::store takes the RootVehicle compound; save no longer takes
a vehicle. sync_level writes the tree for the local player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.getFluidInteractionBox asks the vehicle, and
AbstractBoat.modifyPassengerFluidInteractionBox keeps only the part of the
passenger's box above the boat's top, unchanged when the boat is underwater and
absent when the boat reaches the passenger's top. The rider's fluid interaction
now uses that box instead of clearing the eyes afterwards, so the body
immersion, in_water and the eyes all follow vanilla and a boat floating on water
no longer drowns its passenger (#4001).
Entity.updateFluidOnEyes finds no fluid on the eyes of a rider whose eyes
are inside a boat that is not underwater. The server-side tick of a LAN
guest ignored that, so a boat bobbing in water drowned its passenger.
Quick Actions menu (key.quickActions / G) now reads dialog definitions
from enabled datapacks (data/namespace/dialog/*.json with type
multi_action). Each action entry becomes a button; clicking runs the
action id as a command.
Also exposes DataPack::list and DataPack::read as pub so the game can
scan datapack dialog files.
Co-Authored-By: Nemotron AI agent <[email protected]>
Adds a non-pausing Quick Actions menu opened by the G key (key.quickActions,
already defined in keybinds). The screen shows buttons for quick player
actions and releases the mouse while open. Camera action works; emote,
quick chat and gesture are placeholders pending datapack-driven content.
Co-Authored-By: Nemotron AI agent <[email protected]>
A chunk with a final neighbour (a chunk of an older world, loaded from the
save) used to be left undecorated: no trees, ores or structure pieces in the
strip along the old chunks. The streaming now decorates it with the final
neighbour's blocks in the region and writes what the features changed into
the final chunk, as vanilla's features write into the chunks around them.
The structure starts of an old chunk are not made again from the seed.
test_blending_parity.py --phase full compares the decorated chunks around
old ones with vanilla.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`MinecraftServer.tickChildren` runs `commandFunctions` before any
`ServerLevel.tick`, and `EntityTickList.ensureActiveIsNotIterated` gives
an entity added while the list iterates its first tick on the next tick.
Together they mean a tick function that polls for the drop a block just
made still finds it with `Age: 0` — the pattern data packs use to hook a
block break. Horizon ticked its functions inside the entity pass, after
the items: the drop had counted its age up already and the poll found
nothing.
The functions now run at the top of the tick, before the levels, with
the world's saved data read first (a load tag's `data modify storage`
must land after the save is restored, not before). An item entity added
this tick first ticks the next one.
`type_specific/player` also matches players only, as
`PlayerPredicate.matches` starts with `instanceof ServerPlayer`, and
honours `input` (`InputPredicate`; `input.sneak` is `Input.shift`, the
held key, not the sneaking pose).
Verified with the Vein Miner data pack: its tick-driven chain mining
takes both ores it is asked to, and its `veinminer:sneaking` predicate
reports false while the player is not sneaking.
Co-Authored-By: ZCode Agent <[email protected]>
SavedOldWorld finds the saved chunks with blending_data (or DataVersion
below 4997) per region, reads their border columns and the biomes they
keep, and the overworld generator blends the new chunks around them with it.
tools/old_world_e2e.py plays a world made with the old_world_fixture
example, with and without blending (HORIZON_DISABLE_BLENDING, vanilla's
DEBUG_DISABLE_BLENDING).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Show a dash when no scene-load measurements exist; preserve measured zero and the mean of available scenes.
Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
Blender.of, BlendingData (calculated from an old chunk's border columns),
blendOffsetAndFactor, blendDensity, blendBiome and the carving filter, with
fastutil's entry order for the weighted sums. The generator takes an
OldWorld (the chunks an older version saved) and blends biomes, the density
graph's blend_alpha/blend_offset/blend_density and the carving mask with it.
tools/test_blending_parity.py compares the terrain around old chunks of
another seed block for block and quart for quart with vanilla.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A pack can let players turn its water reflections off: [reflection] option
names an option the mirror is drawn only while it is on (a bool on, a number
above 0); without it, the mirror is drawn whenever the material reads it.
The sample pack shows it.
While mirroring, solid quads were all drawn, since the mirror's eye sees the
undersides the camera does not. Sides::visible_from keeps the quads that face
any eye from the camera straight down to its mirror below the plane, which
differ only in y, for the sections and the distant tiles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTrA2NFEcA5CdtUTS1eH
NoiseBasedChunkGenerator.addDebugScreenInfo: DebugEntryChunkGeneration
adds 'Density N: .. T: .. V: .. C: .. E: .. D: .. W: .. PV: .. PS: ..' (the
overworld's debug_functions, three decimals) at the feet where the game
runs the level's generator: a single-player world on its server thread.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Water can reflect only what is on screen, which loses the sky over a lake and
everything above the picture's top. Within API 0.1, additively, the material
stage gains reflection and reflection_sampler: the world as a camera mirrored
about the horizontal water plane around the camera sees it, sky and solid
terrain at half resolution, cut away below the plane. Frame gains
water_plane, has_reflection and reflection_resolution.
The mirror is drawn only for a pack whose material function reads
reflection (CheckedStage::reads), so other packs and vanilla pay nothing and
the game scans for the plane only then: the median standing-water surface of
a grid of columns around the camera, rescanned when the camera changes block
or each second. Sections are culled against the union of the camera's and
the mirror's frusta; while mirroring, solid quads are not left out by facing
and occlusion culling is off, since both follow the camera's eye.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTrA2NFEcA5CdtUTS1eH
Online servers with enforce-secure-profile=true (26.3's default) refused every chat
message and /msg from Horizon ("Chat disabled due to missing profile public key").
game/remote_chat.rs (ClientPacketListener):
- setKeyPair: after each login packet, and when the key pair is renewed, a chat session
(random id, chain from link 0) goes to the server as ServerboundChatSessionUpdatePacket,
only when the server plays the account under its own profile id: an offline-mode
server's offline id gets none, so chat stays unsigned there as before.
- sendChat / sendCommand: chat and each message argument of a command (SignableCommand.of
over the server's tree) are signed with the same time, salt and last seen, one chain
link each; every packet carries the last-seen update.
- handlePlayerChat / markMessageAsProcessed: other players' packed last seen through the
signature cache, and a ChatAck once more than 64 messages wait.
- sendUnattendedCommand: a clicked chat line, a dialog's button and a mod's command stay
unsigned (vanilla asks before signing one; there is no such screen here).
tools/secure_chat_client_e2e.py: vanilla 26.3's server and a mock of the services that
issues certificates; online with enforce-secure-profile (chat logged as secure, a /msg
whisper delivered, nothing refused) and offline (unsigned, as before). The net harness
takes its chat lines from HORIZON_NET_CHAT and reports whether it signed.
Fixes the LocalChatSession.createMessageEncoder, SignedMessageChain.encoder,
ClientPacketListener.setKeyPair and ClientPacketListener.sendCommand rows of
horizon-rust/horizon#3520 and horizon-rust/horizon#3518.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- auth.rs: the services' certificates endpoint (player/getCertificates, discovery), POSTed
with the account's access token: the PKCS#8 private key, the X.509 public key,
publicKeySignatureV2, expiresAt and refreshedAfter (AccountProfileKeyPairManager.
fetchProfileKeyPair). Fetched on a background thread at start and on each join, kept in
memory like the token (no profilekeys file), renewed past refreshedAfter (checked at
most hourly), and kept as it was when a fetch fails.
- horizon-command: ParseResults::signable_arguments (SignableCommand.of), moved out of
the LAN host's handleSignedChatCommand so a client can run it over a server's tree.
Nothing sends the key yet: the client's wiring follows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The protocol pieces a client needs to sign its chat as vanilla 26.3's does, beside the
server's side that is already here:
- rsa.rs: PKCS#8 (and bare PKCS#1) RSA private keys, as the services hand out a player's
chat key, and RSASSA-PKCS1-v1_5 signatures (Signer.from), checked with the public key
before they leave. Signing is deterministic, so a test compares OpenSSL's bytes.
- secure_chat.rs: SignedMessageChain.Encoder (LocalChatSession.createMessageEncoder), the
client's LastSeenMessagesTracker, and MessageSignature.Packed.unpack against the cache.
Tests run what the encoder signs through the server's Chain, and the tracker's updates
through LastSeenMessagesValidator.
Nothing calls them yet: the account's key pair and the client's wiring follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's material stage cannot tell water from glass, a fluid's top from its
side, or a lamp from stone, and has no surface frame for normal maps. Within
API 0.1, additively, MaterialInput gains face and fluid (0 none, 1 water,
2 lava), emission (the block's HDR tier as 0..1), and the geometric normal and
tangent from the derivatives of the position and uv, snapped to an axis on
block faces so distant faces do not shimmer.
The meshers mark fluid quads in the high nibble of the colour's alpha byte,
which terrain.wgsl does not read (it reads only the emission in the low
nibble); the distant LOD tiles get the same flags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTrA2NFEcA5CdtUTS1eH
A temporal pass (TAA, denoising) needs its own output from the frame before.
Within API 0.1, additively, the post prelude gains history (group 3, binding 8):
after the passes have run, what the last pass read is copied into it, so a
temporal pass put second to last reads its previous result next frame. It is
black on the first frame and with a single pass. Binding 8 leaves 6 and 7 for
the shadow map.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTrA2NFEcA5CdtUTS1eH
Temporal passes (reprojection, motion blur, accumulation) need where a point was
last frame; packs that adapt exposure or fade underwater fog need the eye's light
and medium without their steps. Within API 0.1, additively, Frame gains
previous_view_projection, camera_motion and delta_time, filled by NativeFrame from
the frame before, and eye_light_smooth, in_water and in_lava, eased with
half-lives of 2 s and a quarter second. The frame stays a multiple of 16 bytes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTrA2NFEcA5CdtUTS1eH
The material stage could shade water and glass only from their own texture: it
had no way to know what was behind them, so depth-tinted water and refraction
were impossible. Within API 0.1, additively:
- MaterialInput.screen: the point's place on the screen, 0..1.
- opaque_depth (binding 6) and opaque_color (binding 11): the world's depth and
colour as the solid terrain and entities left them. The world pass ends before
translucent terrain, the targets are copied, and a pass that loads them goes on;
the depth is stored for that while a material stage runs. The colour can be
copied from the scaled world target only: drawing straight to the window, it
is a black 1x1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTrA2NFEcA5CdtUTS1eH
Default off in horizon-graphics.txt. When on, reuse the last shadow map only if
sun/view, world revision, day time, ticks, pack uniforms and settings match,
and skip the cache while entity shadows are drawn.
Co-authored-by: Cursor <[email protected]>
The shadow-cache branch was cut from an older tree and looked like it removed
the published benchmarks folder; put those files back from current main.
Co-authored-by: Cursor <[email protected]>
Basic indirect drawing (glDrawArraysIndirect, glDrawElementsIndirect) is
standard in Desktop OpenGL 4.0+ and GLES 3.1+.
1. In wgpu-hal GLES adapter: decouple DownlevelFlags::INDIRECT_EXECUTION
from compute shaders, enabling indirect execution on Desktop GL 4.0+
(and GLES 3.1+), which unlocks multi_draw in horizon-client-render on
OpenGL 4.2 hardware (e.g. Intel HD Graphics 4000).
2. In wgpu-hal GLES command and queue: avoid pushing draw_count separate
Command variants into the command buffer. Push a single DrawIndirect /
DrawIndexedIndirect command with draw_count. In queue execution, bind
DRAW_INDIRECT_BUFFER once and loop glDraw*Indirect across offsets on the
CPU, eliminating command buffer allocation churn and repeated buffer
re-bindings.
Co-Authored-By: Antigravity <[email protected]>
Twenty-nine literals were drawn straight into the window, so the tables never
had them: the game arguments and proxy rows with their explanations, every
text field's placeholder (one place now), Manage versions, the launcher's own
update lines, the release and patch-note empty states and the sponsors line.
Russian in ru.json; the other six tables carry the English source.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Accent carried one of seven names; it carries a hue now, and none for the
violet, whose tokens stay the palette's. Settings shows a strip of hues
beside the swatches: a click or a drag picks any colour, the swatches stay
as shortcuts, settings.json keeps "violet"/"blue" for those and writes a
picked hue in degrees. The contrast test runs over hues nobody preset too.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Upstream moved page titles and tooltips to translation keys: the profile page gets its own
(horizon.pages.split_profiles).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Light that isn't a block gets HDR output's tier too (entity.wgsl fs_hdr_mark):
- The additive pipelines marked nothing: lightning and the dragon's eyes and death
rays get mark passes of their own (vs_lightning passes the tier on as vs_entity
does). Lightning and the death rays very bright, the dragon's eyes bright.
- End crystals, their healing beam and beacon beams bright. The mark pass now wraps
a beam's v in its sprite as fs_entity does, or it sampled transparent texels.
- Blazes bright, magma cubes and glow squid low, entity flames incandescent.
- Fireworks (sparks and the burst's flash) bright: translucent-layer particles
carry a tier and get a mark pass after their draw.
- Experience orbs very bright, glow item frames incandescent (the item in one stays
only lit).
- The overlay is the layer's low 16 bits everywhere (the entity batch's materials,
the shader packs' plain-run and hurt-colour checks), so a tier never changes how
anything draws, with HDR on or off.
Tuned in game on an AW3423DW (Senjin). HDR off: nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A camel's legs used the default walk: they kept swinging while it sat or dashed and eased in at 0.4. Camel.updateWalkAnimation aims at 6 times the distance moved (at most 1) only in the standing pose with no dash animation running, otherwise at zero, and eases at 0.2. The remote client's own walk update uses it for camels too.
A leashed sitting camel stayed down however hard the lead pulled. Camel.onElasticLeashPull stands it up when it is sitting, not changing pose, and the standing box fits where it is (canCamelChangePose, wouldNotSuffocateAtTargetPose). AbstractHorse.onElasticLeashPull, which it calls first, also ends a horse's grazing.
A horse that landed from a small hop while still rearing ignored the keys until the rear ended, and a donkey or mule never reared on a jump. AbstractHorse.onPlayerJump sets allowStandSliding and calls standIfPossible (every horse that can rear), getRiddenInput only zeroes the input while the horse is standing on the ground without a jump pending and without allowStandSliding, and tick clears it once the horse stands no more.
A horse, donkey, mule or undead horse that reared kept its rider at the fixed seat. AbstractHorse.getPassengerAttachmentPoint adds (0, 0.15, -0.7) times the previous tick's stand animation (standAnimO) to the seat, turned with the horse. The player and a mob rider both slide.
The rider's seat on a camel was a fixed height for sitting and standing, built from the old full-height box, and a mob rider sat at the top of the box when the camel sat. Camel.getPassengerAttachmentPoint takes the body anchor from the current pose box and lerps it through the sit and stand transitions (getBodyAnchorAnimationYOffset), at the camel's age scale. The player and a mob rider both use it now.
A camel that sat down kept its standing box, so it blocked and was hit as if it still stood. Camel.getDefaultDimensions gives Pose.SITTING an adult box 1.43 lower with eyes at 0.845, and a baby box 0.425 tall with eyes at 0.41. Mob::awake_dims now follows the camel's synced pose, on the server and on a client.
A llama landed with the generic fall sound and hurt on any damaging fall. Llama.causeFallDamage makes no fall sound, and hurts the llama and hands the fall to its riders only when the fall is six blocks or more.
A rider took no damage when the mob under it landed hard, and a mob on a mount's back never felt it either. Entity.propagateFallToPassengers hands the fall to every passenger (LivingEntity.causeFallDamage first, AbstractHorse.causeFallDamage after its own hurt and only when hurt), and each passenger runs its own causeFallDamage with its own safe distance. A mount now raises a fall event the game turns into Player.causeFallDamage for the local player and server players, and the mob causeFallDamage for a mob rider.
AbstractBoatRenderer.submit turns a boat that is not under water about the axis (1, 0, 1) by the bubble angle when it is not 0 (Mth.equal, 1e-5), after the hurt tilt and before the model's flip, so a boat wobbles while a column works on it.
AbstractBoatRenderer.extractRenderState copies AbstractBoat.getBubbleAngle(partialTicks), the wobble lerped between the last two ticks, into the render state. boat_geometry now reads it the same way (the drawing follows in the next commit).
The server decides when a column throws or pulls a boat, but the boat its player drives moves on that player's client, so the server's own change does nothing there. AbstractBoat.handleEntityEvent now takes entity events 71 and 72 and runs handleBubbleColumnEffect, which the client applies to the boat it drives.
Boats never rose or sank in a column and a down column let every rider stay. AbstractBoat.tickBubbleColumn now counts the bubble time down on the server while the boat stays above the column, then ejects the riders (down only), broadcasts entity event 71 (down) or 72 (up) and applies the throw or pull. A server's player is let out through guest_vehicle_tick. Clients run the other half: the wobble multiplier grows by 0.05 a tick while the bubble time runs and fades by 0.1 otherwise, and the angle is 10 * sin(0.5 * tickCount) * multiplier.
A boat in an up or down column never changed its vertical speed. AbstractBoat.handleBubbleColumnEffect sets it to 0.6 for an up column and subtracts 0.7 for a down one, on the side that moves the boat (canSimulateMovement).
A boat above a bubble column, with nothing over it, had no bubble state and never splashed. AbstractBoat.onAboveBubbleColumn now marks the boat as above the column with its direction, starts the 60 tick bubble time when it is 0 and, one time in a hundred while the boat is not under water, posts the splash game event on the server and plays the splash sound and particle on the client (it does not push the boat, unlike Entity.onAboveBubbleColumn).
A boat inside a bubble column, with water above it, kept its speed. Entity.handleOnInsideBubbleColumn now moves it up to 0.7 or down to -0.3 (AbstractBoat does not override it), twice a tick as AbstractBoat.tick calls applyEffectsFromBlocks twice. Only the side that moves the boat applies it (canSimulateMovement): the server for a boat nobody drives, the client for the boat its player drives. Boats the window does not tick also get their status refreshed, as getStatus runs on every boat.
Boats had no AbstractBoat.DATA_ID_BUBBLE_TIME (entity data entry 13). The server now sends it with the other synched boat data, and sends 0 again once it drops back, and a client keeps it on the boat.
bubble_column_push found the columns a box touches and pushed in one go, so nothing else could ask which column an entity is above or inside. bubble_columns_touching now returns each touched column with its drag and whether nothing sits above it (BubbleColumnBlock.entityInside); bubble_column_push uses it and behaves as before.
Part of the splitscreen series (#3541). The page a joining splitscreen player picks its
profile on, in horizon-client-render with its own tests, and the profile store it reads:
names kept in the game folder (splitscreen/profiles/<name>/), pages of them, New Profile...
(a player name: 3 to 16 letters, digits or underscores), forgetting one (hidden, its settings
kept), Cancel. Options pages it opens from grey Language and Resource Packs for another
player (the whole process's). Nothing opens it yet: the game side joins next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AMD FidelityFX SDK 1.1.4's frame interpolation (MIT): dilated depth and camera
motion vectors, the estimated depths, the game and optical flow vector fields,
the disocclusion mask, the interpolation and the inpainting, as WGSL compute
passes. Not used by the game yet; the setting and the present thread follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Add ShadowCache struct to store rendered shadow maps between frames
- Cache keyed by sun/moon angle, camera position, and shadow settings
- On cache hit: copy cached textures instead of re-rendering shadow pass
- On cache miss: render normally and store result in cache
- Add pass-timing control command for shader pack GPU profiling
- Expose ShadowTargets fresh/cleared fields for cache access
Benchmarks (seed 1234, render distance 16, time 6000):
- Avg FPS: +23-45% across biomes
- 1% low FPS: +85-167% improvement
- Shadow pass time: ~90% reduction on cache hit
Linux releases ship horizon-launcher-linux-x86_64.AppImage beside the
.tar.gz: one file to download, make executable and run, with the
libraries GPUI needs beside it. glibc, the graphics drivers and the
session's D-Bus stay the machine's (a driver cannot be packaged), so the
image needs glibc 2.35 or newer, like the .tar.gz. appimagetool 1.9.1
is pinned by SHA-256 and run extracted, so building needs no FUSE; the
image itself needs none either, as its runtime mounts it where FUSE
works and unpacks it where it does not.
Inside an AppImage, current_exe names a copy in the read-only mount, so
a launcher in one updates by replacing the file $APPIMAGE names, and
the menu entry it writes starts that file rather than the mount's copy.
The .tar.gz stays what every other launcher updates itself from.
Closes#2110.
Co-Authored-By: DeepSeek V4.1 Flash 1017 (River) <[email protected]>
One Hide, shown after the same three minutes on screen, now sits on the Servers
banner as well as the sidebar card, and hiding either hides both for good.
The on-screen timer counts on every page, so a player who lives in the Servers tab
reaches the Hide too.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The terrain vertex arena only freed a page once every range in it was free, so after a render distance change or far tile churn it kept pages that were mostly empty. On a 2 GiB card the next allocations then spilled to system memory and frames slowed to a crawl. When at least 128 MiB and half of the arena is free ranges, copy the meshes off the sparsest page into the other pages (32 MiB of GPU copies a frame) and let trim free it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The next start removes horizon-launcher.exe.old-* beside it. The launcher
that just restarted into the new binary is still exiting, so it keeps
trying for a few seconds in the background; a file still in use (another
launcher window) is left for a later start.
self_replace copied the launcher under a random hidden name
(.horizon-launcher.<random>.__relocated__.exe), started that copy to wait
for the launcher to exit and delete it, and deleted the copy through a
hidden cmd.exe. Microsoft Defender quarantines that self-deleting pattern
as Trojan:Script/Wacatac.H!ml (#3175).
Windows lets a running binary be renamed, so the running launcher now
steps aside to horizon-launcher.exe.old-<pid>-<n> in its own folder and
the new binary takes its path. No copy is started, and the crate (with
its start-up hook that ran in every launch) is gone from the build.
2026-10-28 13:57:08 +01:00
822 changed files with 193260 additions and 8405 deletions
gh pr create --base main --head "$branch" --title 'Stamp native hook API fragments' --body 'Assign API numbers to the pending native hook fragments. Close and reopen this PR to run CI.'
@@ -742,13 +742,13 @@ Open: the GUI Scale is clamped to 2 after a restart under Hyprland, while XWayla
</details>
#### AppImage and a Nix flake
#### A Nix flake
Install Horizon on any Linux distribution as an AppImage, and on NixOS through a Nix flake.
Install Horizon on NixOS through a Nix flake.
<details><summary>Technical</summary>
A player's idea (#horizon-ideas). An AppImage of the launcher and game from the release workflow, and a flake.nix packaging the release binaries with their runtime libraries (Vulkan loader, X11/Wayland, ALSA), the way horizon-parity's NixOS setup runs them today.
A player's idea (#horizon-ideas). The launcher's AppImage ships with each launcher release ([packaging/linux/appimage.sh](packaging/linux/appimage.sh)): one file with the libraries GPUI needs, which replaces itself when it updates. A flake.nix packaging the release binaries with their runtime libraries (Vulkan loader, X11/Wayland, ALSA) is still missing, the way horizon-parity's NixOS setup runs them today.
description="The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
Loaded 100 of 822 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.