🤖 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]>
🤖 Agent: horizon-prepare names the game files one by one (--version-json, --client-jar, --assets-dir…), still checked against research-lock.json pins; merged #3574's fix and main; horizon-prepare tests and clippy pass.
🤖 Agent: requeued at the author's rebase on main (same change, one line less): vanilla's handlePlayerChat checks (global index, unknown sender, SignedMessageValidator) with chat.validation_error.
🤖 Agent: read the diff: Velocity modern forwarding only with forwarding.secret and online-mode false (the proxy does the Microsoft login); HMAC-SHA256 checked in constant time (RFC 4231 vectors), version, lengths and trailing bytes checked, empty secret stops the start, in-memory joins never forwarded. Wire format from black-box recordings of Velocity 4.3.1. verify green; merging once parity passes.
🤖 Agent: read the diff against vanilla's SulfurCube.knockback helpers (applyHorizontalHitAngleScale, applyVerticalHitAnglePowerTransfer, the position rotation) and Projectile.calculateHorizontalHurtKnockbackDirection; unit test matches the 26.3 helpers to 1e-9, author ran mob_ai arrow damage_source living_hurt server_knockback mob_attributes. CI green, parity runs on the head merged into main.
🤖 Agent: read the diff: an attack or interaction now holds back a move queued behind it until the next tick, as PlayerAction and UseItemOn already do, so the game sees the player as vanilla's in-order packet handling does (falling crit). lan_bot test added; CI green, parity runs on the head merged into main.
#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]>
🤖 Agent: read the diff: native-mod builds run under an ASCII path (resolved data dir, 8.3 short name, or a ProgramData folder named by hash) so MinGW-w64's ANSI tools work for a non-ASCII Windows user name (#5189); tests for the prefix stripping and ASCII check. Approved as code owner; CI green.
🤖 Agent: native mods' block entities reach clients in the chunk and BlockEntityData (empty update tags dropped as BlockEntityInfo.create does); arismoko's replaced-block case fixed and checked in game; main merged and the hook API renumbered to 43. Merging once CI and parity pass.
🤖 Agent: merged main into the branch: main's dedicated event records (chat, join, leave) moved into the shared lan_process_events, and main's flush order (#4765) kept with the packet gate reset at the end of tick_lan. Merging once CI and parity pass.
🤖 Agent: merged main into the branch (item_tooltip.rs: kept both the tropical fish bucket lines and main's map_id_lines). 15 mob fixes, rdbtCVS's points fixed; merging once CI and parity pass on this head.
🤖 Agent: mod registry: a mod may turn native once; a retry takes up its own form branch, proven by an HMAC submission tag under its own derived key (elythepreciouscat's fix for rdbtCVS's point). Registry fmt/clippy/tests pass.
🤖 Agent: #4923 finished: calibration ends off the Options screen and its fullscreen is not saved as the option (rdbtCVS's review). HDR off by default; merging once CI and parity pass.
🤖 Agent: one macOS keychain item per account (the 1 KB split was a Windows Credential Manager limit); rdbtCVS's point fixed in f7f274772: a refused delete writes the whole old layout back, tested halfway and at once. verify green.
🤖 Agent: read the diff: the three recipe helpers now return nothing only on a client with no player, so a server's Game answers them. Tested in game by arismoko (before/after), approved by rdbtCVS; CI green, parity runs on the head merged into main.
🤖 Agent: CI green on 8c913dff6 (clippy, launcher, tests, python, verify), merges cleanly on main after #5217. Telemetry server checked in its own workspace (fmt, clippy -D warnings, tests); the status ping was checked against the real official server. Official SMP count in the launcher and its history on the telemetry server.
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]>
A failed session join goes on for a LAN entry only when an account is
signed in, as vanilla's client always has one (Mojang's rules: no play
without an account that owns the game).
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]>
The test picture and borderless window stayed when the screen left Options
without a Close event (a server opening the death screen), and
observe_fullscreen saved the calibration's own fullscreen as the player's
Fullscreen option. Review points from #4923.
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]>
mob_team scanned every mob for each call, and mob_teams (every tick) and the
name tags and outlines (every frame) call it once per mob: O(n^2) whenever a
scoreboard team exists. The own team is checked first, the tick builds the
tamed owners in one pass, and callers holding the mob pass it.
Co-Authored-By: Claude Opus 5.5 <[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]>
#3727's Audio::share predates #4527's device tracking fields, and #4515's stew
particles call lan_send_particles with #4761's old arguments.
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]>
🤖 Agent: CI green on eccfe3d9b (clippy, launcher, tests, python, verify). Telemetry server checked in its own workspace (fmt, clippy -D warnings, tests). Exact live count, the live count's history and /v1/live/history; #5235 is stacked on it.
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]>
Ludovic (2026-10-31): replace the old invite everywhere. The launcher
reads launcher/links.json from main, so installed launchers pick it up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The detached encoder for saved chunks past the view distance (lan::far)
keeps the mods' update tag hook, which may then run off the server thread.
Co-Authored-By: Claude Opus 5.5 <[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.
- horizon_telemetry::server_status: the server-list status ping (handshake, status
request, JSON answer) and OFFICIAL_ADDRESS, the one place the official server's
address is kept (the launcher's and the game's server lists use it).
- Launcher: "· M on the official SMP" beside "N playing now", pinged once a minute on its
own thread with a 3-second timeout, hidden when it doesn't answer; it opens the Servers
page.
- Telemetry server: pings the official server every minute (HORIZON_TELEMETRY_OFFICIAL_SERVER)
and keeps its player count as the "official" series, like the live count;
GET /v1/live/history takes series=players|official and answers the latest minute too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Tests a_home_behind_a_link_takes_its_packs and a_link_below_the_base_is_refused imported std::os::unix without #[cfg(unix)], breaking cargo test on Windows.
Fixes#5213
Co-Authored-By: Antigravity <[email protected]>
Ports DebugOptionsScreen, DebugScreenEntries and DebugScreenEntryList
from 26.3: every entry can be off, in the overlay or always shown, the
default and performance profiles, debug-profile.json beside options.txt.
The overlay, F3+B, F3+G and the 3D crosshair follow the entries, and the
screen's GUI Scale button edits debugGuiScale.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use a raw mouse click instead of the deferred attack agent task, which releases movement keys before reporting. Prove the click landed from the victim's server hurt log while measuring the first post-hit speed sample.
Co-Authored-By: Codex <[email protected]>
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]>
- GET /v1/live serves exact playing, lan, versions and platforms (live::round is gone);
rare versions are still summed under "other".
- The telemetry server writes the live count down once a minute (totals only: playing,
lan, per platform) into live_minute (7 days), live_hour (90 days) and live_day (kept).
- GET /v1/live/history?range=24h|7d|30d|all serves the series ([start, average, peak]),
the range's peak, the all-time peak and the average per platform, cached a minute, on
the live count's rate limit.
- The launcher's "N playing now" opens playhorizon.dev/stats.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Dispatch HealthEvent::DamageBroadcast and MotionEvent::HurtSound for guests in leave_guest and guest_server_tick (along with record_combat and wear_armor in leave_guest) so every unblocked player hurt immediately sends ClientboundDamageEventPacket with its exact DamageTypeId and broadcasts the hurt sound to nearby players.
- Run tick_guests and the deferred guest attack/effect flushes before lan_flush_effects and lan_swings in tick_lan so captured sounds and mob damage events from guest turns are flushed on the same tick and in the right dimension.
- Attack live mobs directly via attack_mob in guest_attack after within_attack_range instead of turning the guest's view to the mob's center with look_at_entity and re-running raycast pick checks.
- Handle ClientboundSoundEntityPacket in remote.rs and prefer inline RegistryData effects in hurt_sound_for.
Fixes#4330
Co-Authored-By: Antigravity (Gemini) <[email protected]>
Keep the dynamic per-map bounds introduced for prefilled heightmaps, but restore the shared MAPS loop so the optimization does not duplicate the two heightmap paths.
Co-Authored-By: Codex <[email protected]>
random_uuid() seeded each UUID from the clock alone, so calls in the same
clock tick (a microsecond on macOS) returned the same UUID: a pack of mobs
generated together shared one, and a vanilla client keeps one entity per
UUID, so it never moved the others. Seed once, then step one shared
counter through SplitMix64.
Fixes#5207.
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]>
`perf_bench.windows_version` asked subprocess for text, which decodes with
the locale encoding: `pr_ready` runs the tests with PYTHONUTF8=1, so the
reader thread decoded `cmd /C ver`'s OEM output as UTF-8, failed, and left
`stdout` None. `test_report_without_a_load_average` then raised, so every
Windows contributor's self-check showed `Python tests: fail`.
Decode the command's output as OEM, with replacement for stray bytes, and
keep the `platform.version()` fallback for an empty result. The regression
test runs the import in a child process with PYTHONUTF8=1 and checks it still
prints a Windows version; it fails on main with a CalledProcessError.
Fixes#5201
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
🤖 Agent: verified: rebased on main after #5166/#5186, tools/server_admin_e2e.py passed (45 checks: the console attaches, runs say/list, shows their lines, detaches), pr_ready green, CI green.
🤖 Agent: proven on fdfa4fbfa (rebased on main 125875b84): pr_ready --quick green (1427 tests, clippy, fmt, 495 Python tests); CI green (parity 35/35 after a rerun of the known lunge_motion flake, untouched here); anticheat_e2e default set (combat with no server.toml, ground, off, prediction, timer) green; a real vanilla 26.3 client hits a cow with combat on and is never flagged; MSPT 60 bots: anticheat off 3.68/3.69, default 4.11/4.13, 0 flags.
BlockEntityInfo.create sends no tag when getUpdateTag is empty, for every
block entity. The client replaces a mod block entity when the block changes
to another one with the same block entity id, as LevelChunk.setBlockState
removes the old block's.
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)
BlockItem.useOn falls back to Item.use for a consumable whose placement
failed. The server already did, but a server client ended the click
without starting the use, so sweet berries, glow berries, carrots and
potatoes were eaten with no animation, and a released click couldn't stop
it. In adventure mode ItemStack.useOn passes, so UseItem now goes out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Windows user name like "Кириченко Павло" puts the launcher's data under
a non-ASCII path, and the build's MinGW-w64 tools — gcc, as, ld, dlltool —
open files through ANSI APIs, so linking failed with exit code 101 and a
player's own redirection of %APPDATA% changed nothing (dirs::data_dir
reads the known folder, not the variable).
Every path the build's tools see — the toolchain, its Cargo home, the
native/ build tree and now the tools' temp dir, where gcc, as and
cargo install put their scratch files — comes from backend::build_dir:
the data folder resolved (so a junction to a drive with an ASCII path
already works), and when that still isn't ASCII, the folder's short (8.3)
name, or a folder of ProgramData's named for it.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
On macOS, cpal's output_devices() opens every device to see whether it
plays: about 90 ms with twenty devices, once a second on the render
thread. Naming the devices takes 2 or 3 ms, so card_fingerprint now
returns their names there, and the list is looked at only when they
change, as on Linux.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
JSON-RPC 2.0 on stdin and stdout, one message per line, as horizon-ctl mcp.
Each tool is one control API call on the server running in the folder, with
the token read from .horizon/api.json: status, players, run_command, logs,
events, kick, ban, pardon, ban_ip, pardon_ip, op, deop, whitelist, backups,
restore, get_config, set_config, start, restart and stop. stop, restore and
delete_backup need confirm: true; tools that send someone away or can't be
undone carry destructiveHint. The admin e2e bans, pardons, reads events and
is refused an unconfirmed stop through it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: resource packs install for a home behind a link (Silverblue /home, macOS /var), asked for by the orchestrator. directory(base, path) accepts links at or above the owning folder and still refuses a link below it; the canonical path must stay inside the canonical folder. 2 new tests; #5094's test workaround reverted (the tests run under the linked /var again). pr_ready --quick green (port-pack 99, launcher 316); CI green. Builds on gravitacia's #4579 check, credited there.
A catch-up plays every tick recorded since the last one, and `play` moves the
replay to the position each tick claimed. A tick farther than the tolerance
was counted, but a tick after it that agreed from the spot the lie claimed
still replaced `agreed`, so `guest_replay` read a setback point already at the
cheated position: a one-tick lie arriving in a burst with the honest tick
after it counted once and set nothing back.
A catch-up that counts a tick now keeps the agreed point where it was for the
rest of that catch-up. The regression test plays ticks 20 and 21 in one
catch-up, tick 20 claiming +3.0 and tick 21 honest from there, next to
`a_cheat_counts_from_where_it_started`.
Fixes#5132
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
The recent log, then every new line as it happens (the control API's event
stream), a header with the TPS, MSPT, players and RAM refreshed every second on
a terminal (an ANSI scroll region; no TUI framework), and a command line whose
lines run as console commands. Ctrl-D detaches and leaves the server running;
when stdout isn't a terminal it prints plain lines. The admin e2e attaches
through a pipe and runs two commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- 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
🤖 Agent: verified: pr_ready green (36 harnesses affected, all passed), the e2e's 39 event checks passed on the Mac mini; CI caught a race in the stream test (an event between the headers and the first cursor read was skipped), fixed in bef1eff27, CI green.
🤖 Agent: verified: rebased on main after #5165, pr_ready green (64 horizon-server tests), tools/server_admin_e2e.py passed with the CLI checks on the Mac mini, CI green.
resourcepacks::directory refused a link anywhere up to /, so a launcher
home behind one (Fedora Silverblue's /home -> /var/home, macOS's /var ->
/private/var) installed no packs. It now takes the folder that owns the
packs (the work folder or the profile home): links at or above it are the
player's layout and are accepted, every step below it must be an ordinary
directory, and the canonical path must stay inside the canonical folder.
Builds on gravitacia's check (#4579), revised by levzzz5154 (b49925118).
#5094's test-only workaround (resolving the temp dir) is reverted: the
tests run under macOS's linked /var again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MobEffectInstance.CODEC saves ambient and the NBT loader dropped it. A mob keeps
ambient_effects now: loaded, saved, kept through conversion and expiry,
set by /effect give as an instance with ambient false, and the faint
swirls and DATA_EFFECT_AMBIENCE_ID follow it. add_effect_with applies
MobEffectInstance.update's rules for the flags of an effect already there, and
the cloud applies the creeper's effects with theirs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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.
ServerLevel.sendBlockUpdated compares the old and new collision shapes once
per write and only then asks each navigating mob's shouldRecomputePath. The
mob loop rebuilt both shapes for every pathing mob and every write; the shapes
use no entity context and the world is fixed during the loop, so the list of
changed positions is the same for every mob.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic, 10-31: turn the anticheat on. Checks a vanilla client can't trip act by default; the
others run but only log until real players show no false positive. `[anticheat] enabled = false`,
or `enabled = false` in a check's table, turns them off.
New `combat` check, acting by default: vanilla's client attacks only from Minecraft.startAttack,
once a client tick, on the entity of the tick's hitResult, and swings (ServerboundPunchPacket) with
each attack. A client tick with more attacks than swings, or a second entity attacked in the same
tick, flags; from `setback` (2) on the attacks are dropped, `kick` 20. The swing counts before or
after the attack (older clients behind a version proxy swing first).
`timer`, `ground` and `prediction` default to setback, alert and kick 0 (log only); a log-only ground
check keeps the claim. The thresholds once trusted are in docs/anticheat.md.
horizon-bots swing after each attack as vanilla does; --no-swing and --attack A,B are the cheats for
tools/anticheat_e2e.py --check combat, which runs with no server.toml (the defaults).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An event emitted between the reply's headers and the stream's first read of the
cursor was skipped (CI caught it in a_stream_sends_what_happens).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`players [--json]` shows who is online, the operators, the whitelist and the
bans (a stopped server's from its files); `kick`, `ban`, `pardon`, `ban-ip`
and `pardon-ip` moderate the running server through the control API. The
admin e2e runs them too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On macOS, cpal's output_devices() opens every device to see whether it
plays: about 90 ms with twenty devices, once a second on the render
thread. Naming the devices takes 2 or 3 ms, so card_fingerprint now
returns their names there, and the list is looked at only when they
change, as on Linux.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
dealDefaultKnockback takes the direction from the projectile's motion
(Projectile.calculateHorizontalHurtKnockbackDirection, negated), not from its
position. The mob takes it from Mob::projectile_motion, set where a
projectile's hit is dealt (arrows, thrown items, the trident), for every mob
and for the sulfur cube's SulfurCube.knockback and LivingEntity.knockback
branches. Checked on six blows against the vanilla helpers, two from an arrow
with its motion.
Co-Authored-By: Claude Sonnet 5.5 <[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]>
service::tests::units_run_the_supervisor expected the agent's log path
as `/srv/R&D/logs/supervisor.log`, but `launchd_plist` builds it with
`dir.join("logs/supervisor.log")`, which Windows prints with a `\` where
the join adds a separator. The code is right (the launchd agent only
exists on macOS); the test assumed the separator, so it failed on Windows,
where CI only builds and runs no tests.
The test builds the expected string from the same `Path::join`, with the
`&` escaped as `&`, and keeps the literal macOS path under
`#[cfg(unix)]`.
Fixes#5168
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
HoglinAi and Zoglin initFightActivity (MeleeAttack 40 and 15), AbstractCubeMob
push and dealDamage (hurt_touching_golems), EndCrystal.defineSynchedData
(both sides), Enderman.dropCustomDeathLoot (carried_block_drops) and
Strider.getDismountLocationForPassenger (strider_dismount_location). Each
read against the vanilla method.
Co-Authored-By: Claude Sonnet 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.
MobEffectInstance's copy keeps isVisible, so an effect the creeper had
with hideParticles stays hidden on the player or mob the cloud affects.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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]>
SulfurCube.knockback reads source.getEntity(): the player, a mob, a
projectile's owner. Game::attacker_aim gives its eye, view direction and feet
for the blows the cube takes from them (melee, spear, mace, arrows, thrown
items, trident, a mob's attack), and the rotation by height uses the
entity's feet, not the projectile's position. As hurtServer does, a blow of
a type tagged no_knockback (explosions, falls...) does not knock the cube,
and one with no causing entity goes through LivingEntity.knockback.
Checked against vanilla's helper bodies on six blows, two from an arrow.
Co-Authored-By: Claude Sonnet 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]>
GET /api/v1/events streams server-sent events: every log line, the
supervisor's phase, and the game's typed join, leave, kick, chat and death
events with the player's name and UUID (?types= picks some; Last-Event-ID or
?since= resumes). Without Accept: text/event-stream it answers the events
after since as JSON. The dedicated server records the typed events where it
logs them and answers `server-events SINCE` on its control endpoint; the
supervisor fetches them every 250 ms. The admin e2e streams a bot's joins,
chat (a raw chat packet), kick, leave and death.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Creeper.spawnLingeringCloud: right after the blast an AreaEffectCloud of
radius 2.5, wait time 10 and 300 ticks stays where the creeper stood,
shrinking by radius / duration a tick and by 0.5 for each entity it
affects, with the creeper's active effects at 0.25 of their remaining
duration. The potion cloud takes its radius per tick and its effects as
data now, and keeps an infinite effect infinite (withScaledDuration).
Co-Authored-By: Claude Sonnet 5.5 <[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)
SulfurCube.knockback with a block now applies applyHorizontalHitAngleScale
(1.6) and applyVerticalHitAnglePowerTransfer (0.5) with the attacker's eye
and view, then the position rotation, in vanilla's order and float
arithmetic (Vec2.rotate through Mth's tables, Math.atan2). The player's
melee, spear and mace blows carry their aim. Checked against the bodies of
the vanilla helpers run on the real Vec2, Vec3 and Mth classes.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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]>
native_item_entities(dimension, min, max) lists the item entities whose
box meets a box (Level.getEntitiesOfClass(ItemEntity.class, box), as a
hopper's getItemsAtAndAbove asks); native_set_item_entity(dimension, id,
stack) changes one's stack or removes it when empty (ItemEntity.setItem,
discard), as a hopper's addItem does. On the level in place. API 41.
Fixes#5150. Part of #4953.
Co-Authored-By: Claude Opus 5.5 <[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]>
native_level_random(dimension) hands a mod the level's own random
(vanilla's Level.random, level.getRandom()), the stream Horizon's ports
of vanilla draw from, for the level in place and for a parked one. None
on a client of a server and for a level that isn't open. API 41.
Fixes#5145. Part of #4953.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's handleConfigurationFinished builds a new ClientPacketListener,
whose constructor makes an empty ClientWaypointManager. Horizon kept the
last server's waypoints, so behind a proxy the locator bar stayed up on
a server with the locator_bar game rule off.
Refs #4760
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handlePlayerChat checks the global chat index (a mismatch
disconnects), the sender's PlayerInfo, and the sender's SignedMessageValidator:
keyed by the chat session the services' profile-key keys vouch for, or the
fallback of enforcesSecureChat. A message that fails shows the red italic
chat.validation_error in its chat type, tagged chatError
(ChatListener.handleChatMessageError); one a validator takes without a
signature shows as unsigned. The services' keys are fetched once per
connection off the game thread.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_server_about_to_start(dedicated) fires before the worlds load (the
start of MinecraftServer.loadLevel; NeoForge's ServerAboutToStartEvent),
native_server_stopping(dedicated) when an orderly stop begins (the start of
MinecraftServer.stopServer; ServerStoppingEvent), with the players still
connected and not yet saved. Both on the single player server thread and on
a dedicated server. API 41.
Fixes#5138. Part of #4953.
Co-Authored-By: Claude Opus 5.5 <[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]>
recipe_files sorts by the recipe's id, as vanilla lists resources in a map
keyed by it: crushing before crushing/iron, where a path's components put the
subfolder's file first. A subfolder or an entry that can't be listed is warned
and skipped, as PathPackResources keeps what it found; only the top folder's
read still fails. RecipeIndex::load walks the same files, so vanilla's
brewing/ recipes (279 of them, minecraft:brewing/<name>) reach the recipe
book's ids; a brewing recipe has no display of its own, like the special ones.
Review: reservedbytes in the pull request.
Co-Authored-By: ZCode AI agent (GLM-5.3)
Horizon keeps two ports of that method. crates/horizon-world/src/swimming.rs's
`update` carries `!is_passenger()` in both branches and is compared against the
real class by tools/test_swimming_parity.py, whose oracle varies the flag; the
client's copy in motion/fluids.rs::fluid_base_tick left the term out (#4427).
Only the term and the flag that feeds it: main already sits a riding guest up
through passenger_pose_tick, so this drops the duplicate of that.
The new test warms the fluid tick once first — fluid_base_tick samples
was_eye_in_water before it updates the fluids, so a fresh body could not report
is_under_water and the test panicked wherever .local/ existed.
Generated with DeepSeek Harness (deepseek-v4.1-flash)
Co-Authored-By: DeepSeek Harness <[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]>
The last filtered pick()s that compare a distance: a painting or mob
behind a wall in the band between block and entity interaction range
could still be broken or attacked through it. before_block() runs the
same comparison against pick_unfiltered(), as the other entity picks do.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
set_item wrote the merged secret over key before removing key.1, key.2,
...; a removal that then failed left the merged item beside leftover
pieces, and the next read joined them into a secret that doesn't parse.
The account looked signed out, and the fold retried on the corrupted
string until a delete went through and made it permanent. The same gap
was in set_pieces' leftover clean-up on Windows.
Both now snapshot the items first and write the old layout back when a
delete is refused, so a failed write leaves the store reading the
secret it answered before — pieces removed before the failure included.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`GameRenderer.pick` bounds the entity ray by the block hit before
`filterHitResult` drops it past block reach: a wall between the two
ranges still hides the entity. `pick_unfiltered` keeps that traversal
for `before_block` and the deflect and mob-interact occlusion checks;
`pick` keeps the filter only where a block target is consumed.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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]>
Vanilla's ItemStackTemplate.CODEC (and TransmuteResult.CODEC) is
Codec.withAlternative(<id, count, components>, Item.CODEC): a recipe's
"result" is either the stack's fields or just the item's id, so
"result": "mod:item" makes that item. Horizon read only result.id, so a
recipe written that way loaded and made minecraft:air.
horizon_world::loot::recipe_result_id reads both forms, and every place
that reads a recipe's result uses it: Template::parse (shaped, shapeless,
cooking and the special recipes), the banner duplicate and shield
decoration recipes, smithing and stonecutting, the recipe book, the
stonecutter and advancement packets, and the smelting loot modifier.
A result with no "id" (such as the "item" key of older versions) still
loads here as an empty stack; vanilla refuses the recipe. That is left
for its own change.
Tests: the helper, Recipes::load with a bare result, and a content mod
whose recipes make its own items either way (the full content root test).
Fixes#4904
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla writes network NBT (FriendlyByteBuf.writeNbt) through Netty's
ByteBuf, whose writeFloat and writeDouble keep a NaN's bits; only the
disk path (NbtIo.write through DataOutputStream) writes the canonical
NaN. Horizon canonicalized on both. The writer now canonicalizes only for
write_named, and the old test that asserted a canonical network NaN
checks both paths against vanilla 26.3's bytes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The connect flow drew its phase texts as literals on the net thread:
"Connecting to the server..." (also the Remote's initial status), "Joining
world..." and "Finding your friend's world..." stayed English in every
language while the middle phases ("Logging in...", "Encrypting...") were
already vanilla keys. The three now go through `connect.connecting`,
`connect.joining` and Horizon's new `horizon.multiplayer.finding_friend`
(all six bundles), and the local world-open progress line says
`horizon.loading.opening_world` / `horizon.loading.cancelling_world`
instead of hardcoded "Opening world" / "Cancelling world".
`NOT_SIGNED_IN` stays literal on purpose: `reconnect::allowed` matches
disconnect reasons with English keywords ("online mode", "microsoft
account"), so a translated message would slip past the never-retry list.
Matching on keys instead of texts is its own fix.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
`remote_mob_attributes` puts `minecraft:scale` on any living entity, but
`awake_dims_in` grows only avatars' boxes: a scaled strider's `height()`
stays unscaled, so un-divide the default attachment only for them.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Builds keep #4590's profiles, plan and timing and gain #3964's crate
check and post-build steps; post-build programs build with the chosen
profile. The strings #3855 retired stay out of the translations.
#3855 (in #3891) moved Native mods from Settings to the Mods page: the
build's time, how it ended and the Release build switch move with it, to
the instance's Native build row. The install notice #3855 removed stays
removed; the build record keeps #3891's per-mod sha256.
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]>
A display entity carries shadow_radius and shadow_strength, and vanilla
interpolates both across its interpolation window and casts a shadow when
the radius is above zero, through the same gates a mob's shadow uses.
Horizon parsed and blended neither: the two fields sat unread and the
shadow pass collected only mobs, dropped items and experience orbs.
The blend now carries them, the gate arithmetic lives in one function that
both casters call, and text, block and item displays join the pass.
ClientPacketListener.handleProjectilePowerPacket finds the entity by id and,
when it is a hurting projectile, takes the power it accelerates by. Horizon
decoded the packet and dropped it, so a deflected fireball - whose power
halves - kept the spawn default on the client between tracker updates. Our
own server already sends it bundled with the motion.
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]>
native_recipes_of_type, native_recipe_ingredient and native_recipe_result
treated a Game with no player as outside a world. A server's Game (the
server thread, a dedicated server, a LAN host) never has one, so a machine
ticking there got no recipes, though their docs say they are the server's
table. Outside a world now means a client with no player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the vehicle's own position for CONTAINER_OPEN and CONTAINER_CLOSE: vanilla
Entity.gameEvent calls level().gameEvent(source, event, this.position) and
AbstractChestBoat/MinecartChest.stopOpen use position(), not y + 0.5.
Close a vehicle menu through one helper: store the menu's slots back, remove the
shared placeholder, and fire CONTAINER_CLOSE for a chest boat or chest minecart
(no-op for a hopper minecart). The ServerPlayer.tick stillValid close
(out of reach or vehicle gone) now uses it too, so it no longer leaves the
placeholder behind or skips the event, matching closeContainer -> removed ->
stopOpen.
Co-Authored-By: DeepSeek Harness (deepseek-flash) <[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)
Three fixes for text the language screen could not translate:
- The merchant screen drew its title, villager profession, trade level
("Novice".."Master"), the Trades caption, the inventory caption and the
out-of-stock hint through `Enchantments::translate` — a pinned en_us map
loaded once at startup — so they stayed English in every other language.
They go through `lang::get_or` now, like the rest of the menus.
- The reconfiguring screen's button used the key `gui.disconnect`, which
no language file has (vanilla's button is `CommonComponents.GUI_DISCONNECT`
= `menu.disconnect`), so the raw key showed on screen. It is
`menu.disconnect` now.
- Horizon's own strings had no zh_cn bundle: the settings, graphics, HDR,
controller, extras, shader packs, mods and AI agent pages, and the
multiplayer invite dialog, were English for a Chinese player. A complete
zh_cn bundle ships now (the five other bundles already do; the language
is picked up by `Language::load`'s source scan like the rest).
Checked the whole game for the first two classes with a key audit: every
key a `lang` call passes now resolves in vanilla's en_us or Horizon's
bundle, vanilla-namespace keys all have zh_cn values, and no production
text draw keeps a hardcoded English literal. `test_instance_block.invalid_test`
looks like a missing key but is vanilla's own (`TestInstanceBlockEntity`
translates it without the key existing in any language file), so it stays.
Horizon-only command feedback (/raid, /profiler, the agent's messages)
still speaks literal English by house style; localizing those would need
new keys across all six bundles and is left alone here.
Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
The integrated server's owner is saved in the world's own player file;
#4866 stopped writing it on the server thread and nothing caught it
(#5065, #5075). The check moves both players onto glass blocks with
items, saves and quits, reopens and compares (the guest's own file too).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
RecipeBookComponent.mouseClicked keeps the recipe a click placed as
lastRecipe (slot clicks keep it, unlike lastPlacedRecipe), and
RecipeBookComponent.keyPressed places it again on Enter, Space or numpad
Enter (isSelection), key repeats included, with the key's Shift. Holding
Space and shift-clicking the result crafts an inventory quickly. Horizon
had neither the field nor the key path. horizon-ctl's key command takes the
same path, as for the merchant's offers.
lan_e2e: recipe-reselect crafts wool three times from one book click (Shift
held Space, then Enter), on the host and on a guest.
Fixes#5099
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]>
cpal 0.16 lists ALSA devices by opening a hardcoded set of names
("default", "pipewire", "pulse", "jack", "oss") for playback and
capture. Every missing plugin prints two ALSA errors, so the per-second
device check in Audio::should_change_device printed six "Unknown PCM"
lines and spent 5-9 ms (spiking to ~70 ms) enumerating a list that only
changes when the ALSA cards do.
Read /proc/asound/cards (a few microseconds) and enumerate only when it
changes; without a fingerprint, keep checking as before. Open the
default output first and enumerate only when it cannot open.
Co-Authored-By: Big Pickle (opencode) <[email protected]>
Only Blaze and Magma Cube (and burning mobs) were lit fully, and with the sky
light forced to 15 as well. AllayRenderer, VexRenderer and WitherBossRenderer
override getBlockLightLevel to 15 like BlazeRenderer, and EntityRenderer's
default gives a burning entity 15; getPackedLightCoords keeps the sky light
the level's. These mobs were dark in caves.
A phantom's size was never read: /summon phantom {size:N} and a server's
phantoms stayed size 0, so they were never drawn larger (PhantomRenderer.scale
uses 1 + 0.15 * size). Phantom.setPhantomSize (Phantom.java) stores 0 to 64 in
ID_SIZE and updatePhantomSizeInfo sets the attack damage base to 6 + size and
refreshes the box, scaled by 1 + 0.15 * size in getDefaultDimensions. The size
is now loaded and saved as "size", sent to LAN guests as data 16 and read from
a server's.
A block display of a chest, banner, skull, shulker box, bell, conduit,
decorated pot, enchanting table or the end portal and gateway showed only the
empty block model. BuiltInBlockModels gives these blocks a special model, and
CompositeBlockModel.update adds it to the block model's quads. The display now
draws it through the block-entity renderer, as a chest minecart does its
chest.
A dolphin holding an item showed nothing: its model has no arm bones for
ItemInHandLayer's hand lookup. DolphinCarryingItemLayer.submit
(DolphinCarryingItemLayer.java) draws the main hand's item in the GROUND
context at (0, 1, -1), then moves it with |pitch| / 60: y by -0.5 and z by
+0.5 for a negative pitch, y by +0.8 and z by +0.2 otherwise.
Mining a conduit or an enchanting table drew no crack stage on their
block-entity models. ConduitRenderer.submit and EnchantTableRenderer.submit
call submitCrumblingOverlay after the shell (inactive) or cage (active) and
after the book; the wind and eye are not overlaid. render_breaking now
covers both.
A copper golem's item sat at the humanoid hand offset. CopperGolemModel.translateToHand
(CopperGolemModel.java) turns the item 90 degrees about the arm and moves it
0.125 forward while the golem is idle, and otherwise scales it to 0.55 and
moves it (-0.125, 0.3125, -0.1875). Both run before ItemInHandLayer's own
offset.
An enderman with a target (isCreepy) stood still. EndermanRenderer.getRenderOffset
(EndermanRenderer.java) adds nextGaussian() * 0.02 * scale to the render
position's x and z every frame. The offset is added to the entity's render
origin, seeded per mob and frame so the body and carried block agree.
A zombie or skeleton in sunlight with a helmet was simply spared, so the
helmet never lost durability and never broke. Mob.burnUndead hurts the
item in sunProtectionSlot (head; body for a zombie horse or nautilus) by
random.nextInt(2) when it is damageable and only ignites the mob when the
slot is empty. A broken item leaves the slot and
LivingEntity.onEquippedItemBroken broadcasts the break event (49 head,
65 body) for the sound and crumbs.
A grown tadpole was replaced by a fresh frog that kept only yaw, so a
name tag, silence and the like were lost. Tadpole.ageUp calls
Mob.convertTo with ConversionParams.single(this, false, false), which
copies those through ConversionType.convertCommon, then finalizes the
frog for the conversion and makes it persistent.
A piglin, piglin brute or hoglin that zombified outside the Nether was
replaced by a fresh mob that kept only baby, yaw and persistence, so the
name tag, armour, held items, effects and flags were lost. Vanilla runs
Mob.convertTo with ConversionParams.single (piglins keep equipment and the
pick-up-loot flag, hoglins keep equipment only) and then adds nausea for
10 seconds (AbstractPiglin.finishConversion, Hoglin.finishConversion).
ConversionType.convertCommon calls setBaby(true) on the new mob and only
copies the exact age between two AgeableMobs, so a baby hoglin becomes a
non-ageable baby zoglin instead of keeping its growing age.
EnderDragon.aiStep calls currentPhase.doClientTick() on a client and
Horizon never did. DragonLandingPhase spawns 8 dragon-breath puffs from the
head each tick, DragonSittingFlamingPhase 8 x 6 on every second of the first
10 ticks, DragonDeathPhase an explosion emitter every 10 ticks, and
DragonSittingAttackingPhase replays the growl on every tick instead of once.
Vanilla throws away the results of look.yRot, so the puffs keep one direction.
Run them from tick_remote_dragon and drop the one growl the server played.
Breeze.tick (Breeze.java) calls emitGroundParticles while it stands, shoots
or inhales, 20 of them while it slides, and emitJumpTrailParticles for the
first five ticks of a long jump. tick_breeze_effects drew them, but only
tick_creatures called it, so a client of a server (single player included)
saw none. Call it from the remote entity tick too.
The server drew Witch.aiStep's 7.5E-4 roll but never broadcast entity
event 15, and nothing handled it, so no witch ever puffed. Witch.handleEntityEvent
(Witch.java) spawns nextInt(35) + 10 witch particles around 0.5 above its
box, redrawing the count on every loop pass, with gaussian offsets of 0.13.
A zoglin only grunted angrily on its ambient timer. Zoglin.updateActivity
(Zoglin.java) plays entity.zoglin.angry as soon as the brain switches from
idle to the fight activity.
Only players ever drew sprint dust. Vanilla IronGolem.canSpawnSprintParticle
(IronGolem.java) replaces the sprinting test: when its horizontal motion
exceeds 2.5000003E-7 and a one in five roll hits, Entity.spawnSprintParticle
streams the block under its feet back from it, scaled to its 1.4 width.
The killer bunny hop needs only where its target stands, so server_ai_step takes that instead of the whole MobWorld, and the existing tests keep calling it without a world.
Variant 99 (from /summon or a save) only changed the texture. Vanilla
Rabbit.setVariant(EVIL) (Rabbit.java) sets armor to 8, adds the bite goal
(MeleeAttackGoal 1.4), the hurt-by and player/wolf target goals, a plus 5
attack modifier and the Killer Bunny name, and customServerAiStep makes it
face and hop at a target within 4 blocks.
A bucket of tropical fish had no pattern or colour lines. Vanilla
TropicalFish.Pattern.addToTooltip (TropicalFish.java) names one of the 22
common variants, or shows the pattern name and then the base colour (with
the pattern colour when it differs), in italic gray, just before the
instrument line.
Every camel drew its lead at the default offset (eye height, 0.4 of the
width ahead). Vanilla Camel.getLeashOffset (Camel.java) uses the front
body anchor, which sinks while the camel sits, less 0.2 times the age
scale, and 0.56 of the width ahead.
A horn knocked off by a ramming goat was a plain goat horn (always the
ponder tune) thrown with the generic item motion. Vanilla Goat.createHorn
(Goat.java) picks a random instrument from #regular_goat_horns, or
#screaming_goat_horns for a screaming goat, with a random seeded from the
goat's UUID hash, and Goat.dropHorn throws it with deltaX/Z in -0.2..0.2
and deltaY in 0.3..0.7 drawn from the goat's random.
Tadpoles swam around at speed 1.0 and ignored players holding food.
Vanilla TadpoleAi.initIdleActivity (TadpoleAi.java) strolls with
RandomStroll.swim(0.5F) and runs FollowTemptation at 1.25 for items in
the frog_food tag.
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.
Mob.aiStep walks the items within one block of a mob with CanPickUpLoot and
mob_griefing on, and pickUpItem equips what beats what it holds: armour by
armour and toughness, a weapon by attack damage, an equal item by
enchantments, damage and name, with the preferred-weapon tags skeleton and
drowned mobs carry. The replaced piece is dropped unless the drop chance
saves it, and the mob becomes persistent.
Horizon rolled the flag at spawn and nothing read it: a zombie stood beside
a diamond sword and left it there. The comparison rules live in a lib
module so they can be tested without a game, and the pass reuses the panda
and piglin pickup plumbing rather than adding a third copy.
After a /reload that disabled a world's packs, the cached SyncData was
marked as read without packs while still holding the kept registries with
the packs' dialogs, so the next world opened without packs would have been
served them. The cache key now also counts kept pack entries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A forwarding.secret file in the server folder turns it on for an offline
server: the login asks the proxy on velocity:player_info, checks the
HMAC-SHA256 signature and joins the player with the forwarded UUID, name,
properties and address. A login that doesn't answer, or is signed with
another secret, is refused; an empty secret file stops the start.
The format was worked out by recording a real Velocity 4.3.1 with a
stand-in backend (tools/velocity_forwarding_probe.py), see
docs/velocity-forwarding.md.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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]>
Vanilla's MinecraftServer.reloadResources builds the new
ReloadableServerResources against the server's existing
LayeredRegistryAccess (TagLoader.loadTagsForExistingRegistries); the
synchronized registries RegistryDataLoader read at server start never
reload. Players keep the registries of their configuration phase, and
later joiners get the same ones.
Horizon's /reload read the SyncData again with the current packs, so a
dialog file added or removed shifted the dialog indices (vanilla's too,
as pack ids sort among them): /dialog show sent the wrong index, the
re-broadcast dialog tags pointed at wrong entries and new joiners got
another registry.
Now /reload under a hub reloads the SyncData (recipes, advancements,
command tree) but keeps the hub's synchronized registries and their
names, and reads the tags against them (SyncData::keep_registries,
lan::reload_sync_data); the commands' dialog ids are not touched. A
pack's new dialog shows after a restart, as in vanilla. Without a hub,
/reload forgets the cached data as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
changeGameModeForPlayer clears flight on top of the mode abilities when the
new mode is not spectator and the player is in range of the ground:
no collider joins the body, and the block below the feet is within one
block. Horizon copied GameType.updatePlayerAbilities in both the local and
the guest funnel and stopped there, so a spectator switched to creative on
the floor kept hovering.
One probe, in the world crate where the collision scan lives, used by both
funnels.
EntityType.updateCustomEntityTag loads the egg's entity_data component into
the mob it just spawned, after the components, guarded by the type matching
and by the op rule for the three op-only types. Horizon applied the egg's
components and not this one, so a NoAI or Silent egg spawned an ordinary
mob.
The loader is the one /data merge already uses, and the op-only set is now
shared with the tooltip warning that already listed it.
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]>
ResourceManagerRegistryLoadTask.load registers the entries sorted by the
keys of FileToIdConverter.listMatchingResources, the file Identifiers
(dialog/<path>.json), path first, then namespace (Identifier.compareTo).
The ".json" counts: x-y comes before x since '-' sorts before '.'. Every
synchronized registry's entries, the game's own included, now sort by
"<path>.json", then namespace, instead of the bare id.
The cached SyncData remembers whether it was read with data packs. A world
without packs opened after one with packs read it again instead of keeping
the first world's dialogs, recipes and advancements; a world without packs
after another, or after the world-screen preload, still reuses it, and
SyncPreload.adopt replaces only what was read with packs.
The commands' dialog ids are set from the SyncData lan_host and /reload
send, not in finish_open_world: there it loaded SyncData on the server
thread before SyncPreload.adopt, wasting the preload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DebugStickItem.useOn returns FAIL when handleInteraction does not
succeed (no game-master permission, a block without properties), and
handleUseItemOn fires ANY_BLOCK_USE only when the result consumes the
action. use_hand now sets use_failed on the stick's failure, and on a
click a native mod canceled, which uses nothing.
ChiseledBookShelfBlock.useWithoutItem returns CONSUME on an empty slot
without a book, so ServerPlayerGameMode.useItemOn fires DEFAULT_BLOCK_USE
there too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkMap.skipPlayer is a spectator with the rule off, and a skipped player
holds no distance-manager ticket: the world does not load or generate
around them, though they still receive the chunks other players load.
Horizon declared and saved the rule and never read it, so a spectator was a
chunk source like any other player in all three places that matter.
One helper now answers it, and the ticket sources, the LAN streaming areas
and the dedicated server's centre all ask it.
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.
RegionFileStorage.read parses the root straight off the decompressing
stream (NbtIo.read on the chunk's DataInputStream) and closes it, so
bytes after the root and a damaged stream past it are never read.
Horizon decompressed the whole record first and then refused trailing
bytes, so such a chunk failed to load, read_record took it as missing,
and the game generated it again and saved over what had been built.
ChunkRecord::nbt now parses the root from whatever decompressed before
an error (Compression::decode_into keeps it; an LZ4 block is kept only
once checked, as lz4-java hands it out) and ignores what follows. A root
cut short still fails with the decompression error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
AbstractWidget.renderScrollingString / TextAlignment.CENTER uses
x + w/2 - width/2. Flooring (w - width) / 2 sat odd-width labels one
pixel left of vanilla on even-width buttons.
Co-Authored-By: Grok 4.6 <[email protected]>
mob_to_nbt dropped an enderman's carriedBlockState whenever the mob carried
none, but a caller that passes no registry never read the key either, so a
structure template's compound lost a field it had. Reported in review: the
key is only removed when a registry could have written it.
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]>
ClientPacketListener.handleUpdateMobEffect calls skipBlending when the packet
carries no blend flag, and BlendState.setImmediate then sets the factor from
hasEffect rather than fading up. That is the path the effect list takes on
joining, respawning and changing dimension, so vanilla shows the warp at
once. Horizon had this for darkness and not for nausea, whose warp ramped
from zero over a hundred and fifty ticks.
One branch beside the darkness one, in the same place, with the blend-out
guard now shared.
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]>
VanillaTerrain::column read every block of a generated section with
ProtoChunk::block (a section lookup, coordinates and a palette read per
block, ~98k calls a chunk) before repacking it. Light views for the ring
past the view distance are built on the server thread, so flying kept a
steady stream of them there. A whole generated section is now unpacked
word by word and mapped through its palette (TerrainChunk::section_states,
BlockSection::fill_states, PackedStorage::unpack_into); a partly generated
one still goes block by block. Same values, same packing.
Fixes#5038
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
MinecartTNT.getBlockExplosionResistance and shouldBlockExplode: while the
cart is primed (fuse > -1), a rail or a block with a rail above has no
resistance to its blast and is never blown up. Horizon broke both, so a
cart primed on an activator rail tore up its own track and the block
holding it. An unprimed cart (a wall hit, a fall, fire) blasts rails
like any explosion, as before.
Harness: command_exec_corpus `tnt minecart rails`.
Fixes#4992
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]>
chunk_io's PENDING is process-wide, and save tests that fail a chunk load
or write report into it while the chunk_io tests take and compare it, so
those tests failed now and then (#4854). The newest-chunk rules move into
Pending::load_failed and save_failed, which the tests call on a local
value; warn_on_low_disk_space returns what it found. Only the
low-disk-space packet test still reads PENDING, and it checks a flag
nothing else in the lib clears.
Fixes#4854
Written with Claude Opus 5.5 in OpenCode.
LocalPlayer.isSprintingPossible is false while Player.isMobilityRestricted
(blindness). sprint::Conditions already honoured mobility_restricted, but
nothing set it. PlayerMotion now carries the flag into the sprint phase,
and sync_player_body sets it from the effects every tick, on the server
thread's client as well.
agent_control_e2e's consume scenario now holds the sprint key while the
server gives and clears blindness.
Fixes#4831
Written with Claude Opus 5.5 in OpenCode.
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]>
The reload replaces the atlas and the renderer, so the pack's sprite tags
named stale layers and the pack stopped drawing until its files changed.
Suggested by rdbtCVS's review on #4186.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QS9x9HatX1QRXMqxeb
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]>
Right-clicking an empty flower pot with a flower, sapling, mushroom, or other potted plant item now places it in the pot matching vanilla FlowerPotBlock.useItemOn behavior:
- Construct potted_by_content map on Behaviours caching all 39 potted blocks
- Support empty pot plant insertion returning InteractionResult::Success
- Consume 1 held plant item, increment pot_flower stat, and award item_used_on_block
- Clicking a potted pot with a plant returns InteractionResult::Consume
- Add unit test coverage for potting flowers, saplings, and mushrooms
- Update port map entries in game2-013.tsv
Fixes#4844
Co-Authored-By: Antigravity <[email protected]>
/item weapon.mainhand now goes through onEquipItem, as vanilla's setItemSlot
does; hotbar.N stays the inventory's. A content mod's recipes in a subfolder
of its recipe folder load, as a data pack's do. native_net_send_to_server
refuses while the server reconfigures the player. The hook's doc says what
scatters at death isn't item_dropped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
updateFallFlyingMovement takes the look direction from getLookAngle
(calculateViewVector: Mth.sin/Mth.cos, 0.017453292f), the pull-up's
-sin(lean) from Mth.sin, the lift from Math.cos of the widened lean, and
drags by multiply(0.99F, 0.98F, 0.99F). Horizon used exact f32 sin/cos,
f32::to_radians and 0.99/0.98, so a long glide landed up to ~0.11
blocks from vanilla's spot.
Fixes#4618
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
A server's experience orb was only eased towards each position update and
did not move on its own in between, so orbs jumped every update (#4488).
Entity keeps a NO_OP interpolation handler for an orb, so the update puts it
there at once, and ExperienceOrb.tick runs on a client too: gravity,
buoyancy, collisions, friction and followNearbyPlayer towards the nearest
living player within 8 blocks (ExperienceOrb.java). The orb now takes the
update's position, ground flag and motion, and runs the existing
ExperienceOrb::tick each tick. Merging stays on the server.
The local player skipped the fluid update while riding, so its eye flags
froze at whatever they were on boarding and the air bar stayed up inside a
floating boat.
Entity.getFluidInteractionBox asks the vehicle:
AbstractBoat.modifyPassengerFluidInteractionBox gives no box when the boat
is not underwater and reaches the passenger's head, and otherwise only the
part above the boat's top. The rider's tick now samples fluids in that box
(Entity.baseTick) through the existing interaction update.
A client of a server (single player included) clicks the fence block, never
the knot, so the guest's use only reached the block and item turns. The
port of LeadItem.useOn and LeashFenceKnotEntity.interact, Game::interact_knot,
ran only from the host's own interact_entity, so no lead could be tied to a
fence, taken back off a knot or snipped with shears on the server thread.
Vanilla: LeadItem.useOn binds the player's leashed mobs to the clicked fence
(LeadItem.bindPlayerMobs), and LeashFenceKnotEntity.interact handles a click
on an existing knot (LeashFenceKnotEntity.java). A guest's block use now
offers the click to interact_knot first, as the host's frame does.
A guest respawning at a bed or anchor that was gone had the
"block.minecraft.spawn.not_valid" message put into the host's chat, and
never saw it itself. PlayerList.respawn sends the player
ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, and
ClientPacketListener.handleGameEvent turns it into that system message.
Send the event from both guest respawn paths when the point cannot be
resolved, and show the message when the remote client receives it.
Game::tick_scoreboard recorded these criteria only for the in-frame
player, so a guest's (and a single-player owner's, who is a placed guest)
health, food, armor, experience and level scores never moved: a
`health` objective shown below the name or in the tab list stayed at 0.
ServerPlayer.doTick calls updateScoreForCriteria for every player when
the value differs from lastRecorded*. Move that part into
record_player_scores, used by tick_scoreboard and by each guest's tick
with a per-guest record in GuestPlay (reset with a new body).
Minecraft.startUseItem returns when LocalPlayer.isHandsBusy (rowing with
the movement keys held). The in-frame path checks it in use_with_hand;
remote_use did not, so on a server a right click while rowing still ate,
placed or threw. remote_use now returns the same way.
Item.use runs Equippable.swapWithEquipmentSlot on the client too
(MultiPlayerGameMode.useItem), so the worn piece and the hand change at
once and the server's slot packets only confirm. remote_use_item sent the
packet and waited, so on a slow server the swap showed a ping late.
It now calls equip_from_hand like the in-frame path. A client spawns no
dropped item (the server does) and takes its attribute modifiers from the
server's update packet, so equip_from_hand skips both when remote.
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]>
Zombie.finalizeSpawn and AbstractSkeleton.finalizeSpawn put a carved pumpkin (or jack o'lantern) on an empty head 25% of the time on 31 October. Structure spawn already did this; natural, spawner, summon and raid paths never ran it.
Batch movement, teleport and attack commands so the check proves that a hit occurred instead of overshooting the target between control calls. Assert the first post-hit sample is below sprint speed.
Co-Authored-By: Codex <[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.
zip 9's enclosed_name turns ../x into x instead of refusing it, so the
escape test main added passed the entry through. Check the stored name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
IronGolem.canAttack answers no for a creeper as well as for a player, and
that answer is asked by the retaliation goal, by target continuation and by
the nearest-target search. Horizon mirrored only the player half, so a
creeper that hit a golem became its target and the golem swung back.
The rule now covers any target and is asked by the retaliation goal and by
target continuation, the two places that had no answer.
The hand pass draws with the camera's view, whose rotation spreads a ridden
boat's turn over the frames of a tick (LocalPlayer.getViewYRot). hand_geometry
posed the hands with the end-of-tick yaw instead, so they swung by that turn
20 times a second. HandSway::to_world takes the camera now.
Fixes#4583.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeutralMob.isAngryAtAllPlayers is the gamerule universal_anger plus a mob
that is still angry with no anger target left, and isAngryAt asks it before
the named target. Horizon registered the rule and stored it, but the wolf's
player target goal only checked the named target, so the rule did nothing.
The wolf's predicate now asks the rule, and the world it reads carries the
gamerule the same way mob_griefing and mob_drops do.
Use the entity kind for the RemotePlayer client-side attack path so profile bookkeeping cannot suppress the vanilla attacker slowdown. Restore the client-side mob warning and leave sprint synchronization to the normal end-of-tick movement send.
Co-Authored-By: Codex <[email protected]>
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.
zip 2 refused an archive whose entry claims method 99 without an AES
field; zip 9 opens it and only the entry fails, which mirror now skips.
A name that runs past the archive still fails the open, and the build.
Also rustfmt for upgrade/mod.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fills in the 135 strings the table lacked (Mods browser, native mods from
GitHub, AI agents, data collection), drops the unused "Sign in", names pt-br
in the language setting's hint, and formats parse().
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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]>
world_wide_plans_stay_small_and_cover_every_level only printed node counts.
It now checks that the roots reach MAX_LEVEL, that each plan stays under
16 384 nodes (10 655 at most today, at a corner of the world), that reduced
has no more nodes than full, and that every level from the finest one to
the roots has tiles. The mesh test's corner check bound as (A && B) || C;
it now needs the far corner. Main's mesher materials take a position: the
heightmap's fluid test passes the tile origin, as fill_buried_gaps does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Dynamically clamp the occlusion graph search box height to the highest non-air section across columns in the search radius, skipping empty air section exploration.
- Fall back to column reachability for open air queries above the clamped search box.
- Add unit test vertical_clamping_bounds_height_and_reaches_open_sky.
Fixes#4985
Co-Authored-By: Antigravity <[email protected]>
Mob.aiStep's looting loop runs for every server mob, AI or not, and an
allay's pickup gates (canPickUpLoot, the held item, mob griefing, the
inventory room) never ask for AI. So a NoAI allay still takes a matching
drop that lands in its getPickupReach, which Allay overrides to (1, 1, 1)
— it only never walks to one (the sensor is the brain's) and never throws
one (the delivery goal is too).
The blanket skip becomes that split: the reach is one helper, the search
uses it when AI is off, and delivery stays behind the AI gate.
AbstractContainerMenu SWAP button 40 is the off hand even when the
open menu has no off-hand slot. Horizon only mapped 40 on the
inventory menu, so F in a chest predicted a move the server ignored
and left a ghost item.
Fixes#4887
- 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]>
Keeps both sides: main's owner updates, the "listed by its repository"
refusal and the label route next to the native turn and the retry. A mod
turning native is never an owner update (its kind changes, which the
owner-update workflow doesn't review), and the test checks for it.
Co-Authored-By: Claude Opus 5.5 <[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 reader that returns a Result fails with an error; the best-effort port-pack scans, which already skip entries they can't read, skip it. Also ports the readers main added since (resourcepacks.rs, upgrade/mod.rs) to the 9.x API.
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]>
Pufferfish.addAdditionalSaveData writes PuffState and reads it back clamped
to two; Enderman writes carriedBlockState with the block state codec, only
while it carries one, and an air state reads back as no block. Horizon
saved neither, so a puffed fish came back small and an enderman forgot its
block across a reload.
The block state codec moves from the save path into entity_nbt so both
sides share it, and the registry it needs is threaded to the two save
functions.
#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]>
Game release notes open and close with the warning to get the launcher and not run the files; the closing one sits above the disclaimer. Rebuilt on main's release notes.
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The release switch picks Release; Fast still falls back to release for a
revision without [profile.fast]. The log filter now strips only the progress
pieces of a line, so an error after a redraw reaches build.log.
Co-Authored-By: Claude Sonnet 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]>
passenger_attachment multiplies the base attachment by the entity scale; the bounding-box fallback now reads height() already scaled after getDimensions learned minecraft:scale, which double-applied it for players and mannequins.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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]>
When landing a melee attack while sprinting with attack strength scale > 0.9,
vanilla Minecraft 26.3 executes a sprint-knockback hit:
- Plays minecraft:entity.player.attack.knockback
- Multiplies attacker horizontal velocity by 0.6 (ATTACKER_SLOWDOWN)
- Cancels sprint state and syncs ServerboundPlayerCommandPacket(STOP_SPRINTING)
- Excludes spears from sprint-knockback attacks
Fixes#3736
Co-Authored-By: Antigravity <[email protected]>
Secrets were split into ~1 KB pieces on every OS for Windows Credential
Manager's 2560-byte item limit. Each piece is its own keychain item and
asks consent for a launcher signature it doesn't know, so macOS players
got a prompt per piece after every ad hoc self-update (#4912). On macOS a
secret is now one item; split pieces an older launcher left fold into it
on first read, and a merged item reads fine as "piece 0" to launchers
that still split. has_owner also checks the selected account first, so
the owner gate touches one item when the player owns the game.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Each content block gets a minecraft:block_entity_type id after vanilla's,
named after the block. The chunk encoder sends a mod block's block entity
with that type and the tag native_block_entity_update_tag gives
(getUpdateTag), and leaves out a mod block that has none; BlockEntityData
on a block change uses the same hook. The client makes its copy through
native_block_entity and merges the tag into its nbt. NATIVE_HOOKS_API 23.
Refs #4615
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- post_builds: only a missing native-post-build.json means no steps; any other read error, or
a mod id that isn't one, stops the build (arismoko 3, dimaniojk 1, small point).
- native_data::built and native_built_data return None when the game's path is unknown,
never a folder relative to the working directory (arismoko 5, dimaniojk 2).
- .hnmod: data is only a bundle's data/ or a crate's top-level data/ (arismoko 4); README*,
LICENSE*, COPYING*, NOTICE* and .gitignore are accepted in the bundle or a crate folder
(reservedbytes 3).
- Post-build steps get HORIZON_NATIVE_BUNDLE_DATA_DIR, the bundle's staged data/ (reservedbytes 4).
- "Building horizon-mixins (once)" shows only when it is built; the crate check runs only when
a mod has a crate; building a post-build step has its own label (reservedbytes 5).
- launcher/Cargo.toml: the libc block above the keychain comment (DUzzL 2).
- docs/native-mods.md: the "A bundle" section (crates, data files, post-build steps), lost in
an earlier merge, is back, with the .hnmod limits, run-time data and why Horizon's own
crates are refused for now (#3897; arismoko 1, dimaniojk 3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
stack_to_network writes a fixed list of components, and single player's
window gets its stacks through it from the integrated server, as LAN
guests do. custom_model_data and item_model were not on it, so the
client's item model never saw them: custom_model_data tints stayed on
their default, selects fell back, and an item_model override was ignored.
Write both: item_model as its id, custom_model_data from its saved form
(CustomModelData.CODEC, colours as ints or float triples) into its stream
codec.
Co-Authored-By: Claude Opus 5.5 <[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
Graphics: HDR gets a calibration for Black Level, Peak Brightness and Paper White:
each step a test picture drawn by the HDR pass straight in nits, with a slider
and one line of help. Horizon's logo disappears on black, then into a patch of 10%
of the screen; Paper White is a white card, by eye. Save writes the three, Cancel
keeps them, a step left untouched keeps Auto. Paper White goes down to 10 for
dark rooms.
Where the system reports the display's values the button reads Calibrate Anyway...
with "Already set up for your display", and Use Automatic Setup puts changed
values back; where nothing is reported it asks the player to calibrate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
StructureCheck.tryLoadFromStorage and onStructureLoad take the references of
a chunk's saved starts as known, so a world saved by vanilla keeps the
starts its maps already pointed at. SaveDir::start_references reads them;
the sidecar file still holds the ones maps add here.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A lobby's giant fake player (minecraft:player or mannequin with a
minecraft:scale attribute) rendered huge but kept a 0.6x1.8 hitbox at its
feet, so right-clicking it did nothing while vanilla's scaled box answers
the click. mob_hit also skipped non-alive mobs although CAN_BE_PICKED is
isPickable = !isRemoved, hiding health-0 NPCs from clicks and the entities
command, and entity reach was hardcoded instead of the
entity_interaction_range/block_interaction_range attributes.
Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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]>
Keep both sides in boats.rs: upstream/main's AbstractBoat.remove lead drop
(boat_removed) and this branch's chest-content spill (unconditional) plus the
entity_drops-gated boat item drop.
Co-Authored-By: DeepSeek Harness (deepseek-flash) <[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]>
A crash mid-write left truncated JSON, which loads as no references and
made maps repeat structures.
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]>
- Pre-zero reused thread-local scratch density buffer in fill_noise before density sampling.
- Track heightmap bounds dynamically with re-queried first_available across downward scans and non-opaque blocks.
- Add regression unit tests for poisoned scratch density buffer reuse and top-down surface heightmaps.
Co-Authored-By: Antigravity <[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]>
Drain hub player packets before scheduled block ticks on the server thread so a second sand place in the same tick as the first's fall schedule still sees the support, matching vanilla's packet-before-block order.
Co-Authored-By: Composer <[email protected]>
Co-authored-by: Cursor <[email protected]>
Single-player often had a skin but no cape because the client only filled missing skins from the services, not missing capes. Ask GET /minecraft/profile once after join and merge ACTIVE skin and cape into local textures.
Co-Authored-By: Cursor <[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]>
An --output naming root/.local itself took that folder's lock a second
time through another handle, and waited for its own process forever.
Java found through a relative JAVA_HOME, --java-home or PATH entry is now
absolute, since the exporters run in .local.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CombatTracker only tracked mob attackers, now we track players so we dont
print '%1$s was slain by %2$s'. pr covers melee hits, sweep attacks,
arrows, thorns.
- 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]>
Identical stills share a layer under several names, so sprite_tags gave
the generated switch duplicate cases (naga: ConflictingSwitchCase) and the
pack failed to load. Their tags are now merged per layer. Reported with
the fix by DUzzL's agent on #4186.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QS9x9HatX1QRXMqxeb
Right-clicking a chest boat, a chest raft, a chest minecart or a hopper
minecart did nothing: the server-side container path (Game::lan_open_container,
shared by LAN guests and single-player players) only knew block containers and
silently dropped the vehicle's placeholder key.
- give a container vehicle its own menu and title: ChestMenu.threeRows (9x3,
27 slots) for chest boats, rafts and chest minecarts, HopperMenu (5 slots)
for hopper minecarts, titled with Entity.getDisplayName's name key, as
AbstractChestBoat.createMenu / MinecartChest.createMenu /
MinecartHopper.createMenu / MenuProvider.getDisplayName do;
- fire CONTAINER_OPEN with the player and PiglinAi.angerNearbyPiglins on a
chest boat and a chest minecart (MinecartChest overrides interact; a hopper
minecart does not), and CONTAINER_CLOSE on their close (MinecartChest
overrides stopOpen; a hopper minecart inherits the no-op);
- send OPEN_INVENTORY while riding a chest boat (HasCustomInventoryScreen ->
ServerGamePacketListenerImpl.handlePlayerCommand), so E opens the chest
instead of the player's inventory;
- spill a chest boat's contents on every destroying removal, as
AbstractChestBoat.remove does (a creative hit discards, DISCARDED
shouldDestroy is true, and Containers.dropContents has no entity_drops
gate; the boat item stays on the entity_drops death path);
- drop the chest minecart's open/close chest sounds: vanilla plays none.
Tests: tools/test_vehicle_container_parity.py (new, 13/13 menu cases against
the real 26.3 jars), live before/after on the server thread, and two-client LAN
item-loss/duplication runs.
Refs #4370
Co-Authored-By: DeepSeek Harness (deepseek-flash) <[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]>
fast (incremental, no LTO) is the default Cargo profile of native builds; a
setting, native-build --profile and 'settings native-profile' pick release.
The profile is in the key and has its own target folder. CARGO_INCREMENTAL is
removed from the build's environment. The build says up to date / incremental
(what changed) / first build before it starts, shows crates done of the total,
the profile and the time while it compiles, and 'Built in 42 s (fast,
incremental)' after.
Co-Authored-By: Claude Sonnet 5.5 <[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]>
Display.readAdditionalSaveData reads "transformation" with
Transformation.EXTENDED_CODEC (the four-part form or an ExtraCodecs.MATRIX4F
decomposed by MatrixUtil.svdDecompose) and falls back to
Transformation.IDENTITY. The comment above the read still described only
the four-part Transformation.CODEC; it now names the codec actually followed.
The MatrixUtil.svdDecompose test pins its expected bit patterns to u32 so the
0x8000_0000-style literals never fall back to i32.
Co-Authored-By: Claude Opus 5.5 <[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]>
Display.readAdditionalSaveData reads "transformation" with
Transformation.EXTENDED_CODEC: the translation/left_rotation/scale/
right_rotation record, or else ExtraCodecs.MATRIX4F (sixteen floats,
row-major), which new Transformation(matrix) splits on demand with
MatrixUtil.svdDecompose (eigenvalueJacobi on a^T*a for the right
rotation, then QR Givens rotations about z, y and x for the left rotation
and scale). Horizon left a matrix-form display at the identity.
- horizon-assets math: svd_decompose, following MatrixUtil.svdDecompose,
stepJacobi, approxGivensQuat, qrGivensQuat and GivensParameters with
JOML 1.10.9's Matrix3f.mul/rotate and Quaternionf.mul arithmetic; the
test checks the bits vanilla's own decomposition returns.
- text_display: the matrix alternative feeds DATA_TRANSLATION_ID to
DATA_RIGHT_ROTATION_ID. Axis-angle rotations now follow
new Quaternionf(AxisAngle4f) (the angle wrapped to [0, 2pi), the axis
not normalized) and component rotations Quaternionf.normalize, through
the existing horizon-assets helpers, instead of an approximation.
- entity_data: a test pins VECTOR3 and QUATERNION to
FriendlyByteBuf.writeVector3f / writeQuaternion's big-endian floats.
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.
RegistryDataLoader builds the synchronized registries from every enabled
pack, so a pack's data/<ns>/dialog/ files join minecraft:dialog in
Identifier order (path first, then namespace) and /dialog show can open
them. The data pack loader now reads dialog/ files, SyncData merges them
over the game's own and always sends their data (no client knows the
pack), and the commands' dialog ids follow SyncData so a parsed id sends
the index the clients hold.
Opening a world with data packs now drops the preloaded SyncData, which
was read without the world's packs and kept their recipes, advancements
and dialogs from the clients.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayerGameMode.useItemOn fires DEFAULT_BLOCK_USE when the block's
useWithoutItem consumes the click, and handleUseItemOn fires ANY_BLOCK_USE
when the whole useItemOn does, with the hand's stack as the use left it.
Behaviours::use_item_on_branch says which of useItemOn and useWithoutItem
answered; the generic path now also fires ITEM_USED_ON_BLOCK for a
consuming useItemOn (a candle snuffed by hand). The jukebox eject, the
decorated pot wobble and the chiseled bookshelf removal fire
DEFAULT_BLOCK_USE on their useWithoutItem branch.
Fixes#4499
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
MushroomCow.getEffectsFromItemStack reads the item's block, so a brown
adult takes a flower, stores its suspicious stew entry and hands it back in
the next bowl of stew as a suspicious stew carrying that effect. Horizon
had the component, the brown variant and the bowl branch, but no flower
branch and no stored effects, so a bowl always gave plain stew.
The effect belongs to the block, not the item, so the table lives beside
the other block facts: seventeen entries, cross-checked against the jar's
own suspicious_stew_from_* recipes. The two eyeblossoms and the wither rose
are holders too, which a count of FlowerBlock constructor sites misses.
- Precompute the 48-bit masked seed base in LegacyPositionalRandom to eliminate full multiplicative seeding on position and string queries.
- Reuse thread-local scratch density buffers in fill_noise to eliminate 393 KB heap allocations per chunk pass.
- Hoist column density slices in fill_noise to eliminate 98,304 redundant volume.index() lookups.
- Implement monotonic top-down heightmap termination in fill_noise, bypassing all subterranean heightmap checks once settled.
- Cache horizontal squared distances and cell coordinates in NoiseBasedAquifer across vertical slabs, reducing inner distance loop work to a single subtraction and addition.
- Early-return in apply_carving_mask when the carving mask is empty to avoid redundant MaterialConditions setup and graph compilation.
Co-Authored-By: Antigravity <[email protected]>
Review (rdbtCVS): the "Submitted by @<login> through the form." line is
plain text anyone who writes to the registry repository can copy, and a
commit's author can be set by anyone too. The form's commit now carries
a "Submission: <tag>" trailer, an HMAC of the branch and the login with
the service's key; a stale branch is started again only when it holds no
commit of its own or the one commit with this account's tag. The test's
someone-else branch now copies alice's exact words with a forged tag and
is refused without a reset.
Co-Authored-By: Claude Opus 5.5 <[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]>
publish() refused any kind change ("blockscope is a mod"), but the
registry's own review (horizon_mods::index::registry::review, run by
horizon-mods' CI) and docs/native-mods.md let a WebAssembly mod's entry
turn native-mod once, never back. The form now allows exactly that, by
an owner, and names the rule when it refuses any other kind change. The
pull request says the project turns native.
A submission GitHub broke off after making its submit/<id>-<version>
branch was refused as "already submitted" (409) on retry, with no pull
request. When the branch exists, the form now looks for an open pull
request (still 409, with its address) and at the branch's own commits:
if they are all this account's form commits (or there are none), it
resets the branch to main and submits again; anyone else's is 409.
Co-Authored-By: Claude Opus 5.5 <[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]>
main added refill_own_store, which called the launcher's lacks_languages,
gone since missing_extras became the one check.
Co-Authored-By: Claude Opus 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]>
Main took API 3 (world lifecycle) and 4-7 (arismoko's server tick, block reads,
neighbour and block-change observations). The data-file helpers (native_data!,
native_built_data) are API 8. MIXINS_REV moves to horizon-mixins 5140cdf
(#5, #7, #8 and #9: RETURN hooks on functions with closures and references).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Merges #3574 (and with it #3567 and #3551) for docs/api/launch.md.
- --java-home names a Java the launcher manages (Options::java_home was
there for horizon-launcher only).
- The import logs the client jar it read, which --client-jar may have
named outside the Minecraft folder.
- named_asset_store_serves_the_asset_commands: with an empty Minecraft
folder, an asset command fails without --assets-dir and copies from
the store it names.
- docs/api/launch.md and native-client.md: the named inputs, that an
earlier verified import is reused instead, the server jar rule (Horizon
never downloads it; a launcher only for an owner), --java-home.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- docs/api/launch.md gets a Preparing section: --output, --check (what it
tests and what it doesn't), --complete, and --progress json's rules:
one JSON object a line, the three keys, readers ignore what they don't
know, a new key is a patch of the Launch API.
- The changelog lists the preparer's options under 0.1.0, still
unreleased. The overview, docs/api/README.md and stability.md say the
Launch API covers the preparer now.
- progress_json_lines_keep_their_shape pins message, error and fraction
(a quote, a newline, three decimals); fraction_line is the one place
that formats a fraction.
- steps_open_every_stage reads lib.rs with either line end, so it passes
on a Windows checkout too.
This branch now holds #3551 as well, for docs/api/launch.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A preparation removes the stamp before it writes anything and stamps
again at the end, so --check calls a folder being prepared, or whose
preparation stopped, not current.
- Each preparation holds .horizon-prepare.lock in the resources and in
root's .local (add_assets in the resources); a second one says it is
waiting and waits. tools/prepare_output.py leaves the lock out of the
digest.
- missing_extras is the one completeness check: --check and the
launcher's COMPLETIONS both use it.
- The Java exporters and the worker run in root's .local, so vanilla's
logs/ stays in the scratch, not where the preparer was started.
- --check's help and docs/native-client.md say what it doesn't test
(contents) and that exit 1 is either gaps or "can't check".
Co-Authored-By: Claude Opus 5.5 <[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]>
MIXINS_REV moves to horizon-mixins d9c83d7 (#7 and #8 by reservedbytes):
- a crate dependency with default features (hashbrown = "0.15") builds: cargo adds the
lock links between crates Horizon already locks, any new crate is still refused;
- a post-build bin named like another mod's step bin or like horizon-client is refused
(E_POST_BUILD).
The launcher also copies the game and each step's program out of the shared target folder as
soon as it is built, so a bundle staged by an older horizon-mixins can't run another mod's
program or replace the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkGenerator.findNearestMapStructure with createReference: a map with
skip_existing_chunks (the default, and every cartographer trade) points at
the nearest start no map pointed at yet and counts it as pointed at
(StructureStart.addReference, getMaxReferences 1), so two maps made in a
row no longer lead to the same structure. The starts are kept per
dimension in data/horizon_structure_references_<dim>.json.
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]>
Servers place HUDs with their pack's text.vsh (glyphs far right of a boss bar title, moved to the screen's corners) and colour effects in text.fsh. horizon-shaders translates the pack's GLSL (#include or #moj_import) to naga; the HUD draws GUI text runs with it.
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
ServerPlayer.isInvulnerableTo has three terms, and the third is
!connection.hasClientLoaded: from a death until the client reports the
world loaded, nothing hurts the player. That is what spares a respawned
player the void in the tick it is still standing where it died.
Horizon implemented all three terms, but the only production
implementation of the trait returned Ok(true) for this one, so it was
dead code. A guest now carries the flag: cleared by a respawn, set by
ServerboundPlayerLoadedPacket, copied onto the body each turn, and read by
the damage environment (including the starvation path, which builds one
too). A level change does not clear it, because vanilla resets the loaded
state on death and respawn only.
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]>
The villager changes (crossed arms item, held offers, equipment sync) now
live in their own branch fix/split-villager-trades; this one keeps the map
banner and frame markers.
Keep main's held-stack API (set_main_hand, main_hand_stack) and bone meal
prediction; the villager's shown item now lives in the main hand stack, and
the equipment sync sends that stack.
Packs declare [[tags]] of block sprites (glass, say) and read them as
MaterialInput.tags; the frame gains precipitation, temperature, downfall
and wetness (rain smoothed as the shaders/ format's).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QS9x9HatX1QRXMqxeb
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]>
Review on #3777: worlds are small ids now (Mixer::world_id), so the callback compares and
copies integers; its camera list is a buffer the mixer keeps; a sound only looks for another
player's copy (and takes the lock for it) while another player shares the output.
Measured with 32 playing sounds and one player, 512-frame buffers (release, M-series Mac):
upstream main 21.9-22.5 us per buffer, this part before 21.3-21.8 us, after 21.3-21.6 us:
the same within noise; the time goes to mixing the samples.
Co-Authored-By: Claude Opus 5.5 <[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 bundle's [[crate]] (staged by horizon-mixins 0.6.0 as native-crates/<mod>/<crate>/)
may use only the registry crates Horizon's Cargo.lock has: the build checks the staged
lock's downloaded packages against Horizon's and fetches with --locked, saying plainly
when a feature would add a crate. After the game compiles, each [[post_build]] bin is
built with the same profile and run in the new version folder with
HORIZON_NATIVE_{MOD,VERSION_DIR,DATA_DIR,CACHE_DIR}; a failure fails the build.
Progress steps are translated. MIXINS_REV moves to horizon-mixins#5's head.
A test bundle (launcher/tests/fixtures/native-crate-demo) uses all of it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_data!("<mod>", "file") compiles a file of a bundle's data/ folder
(staged as native-data/<mod>/ by horizon-mixins) into the game;
native_built_data is the folder a mod's post-build step wrote into. A
.hnmod may carry a crate's own data/ folder too.
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.
A filled map's tooltip showed none of its lines. MapId.addToTooltip gives Unknown Map when the client has no data for it, else the id line (not for a named map or one a cartography table is locking or scaling), Locked, and with advanced tooltips the scale and level, a scaling copy counting one more up to 4 (MapId.java:27). The lines sit between the instrument and bees lines as in ItemStack.addDetailsToTooltipComponents.
Taking a map out of an item frame, breaking the frame, a blast or /kill left the frame's marker and saved entry on the map. ItemFrame.removeFramedMap calls MapItemSavedData.removedFromFrame(pos, entityId) on every path that empties the frame (dropItem unless the drops are off and something caused it, onKilled), which removes the marker and the frame entry (MapItemSavedData.java:421, ItemFrame.java:247).
A map hanging in an item frame never showed the frame's marker, and a player who saw it framed without carrying it kept a marker. ServerEntity.sendChanges ticks every 10 ticks tickCarriedBy(player, stack, frame) for each player: one carrying no copy loses their marker, and with position tracking the frame's marker is added under its entity and the frame saved with its block, rotation (2D direction times 90) and entity, replacing an older entity's marker at the same block (MapItemSavedData.java:159).
A player wearing a carved pumpkin was still drawn on other players' maps. MapItemSavedData.tickCarriedBy removes the marker of every other holder for whom hasMapInvisibilityItemEquipped finds a piece of #map_invisibility_equipment in an armor slot (MapItemSavedData.java:225). Each holder keeps what it wore on its own tick.
A banner marker stayed on the map after its banner was broken or changed. MapItem.update calls MapItemSavedData.checkBanners for every column it reads: a marker in that column whose banner is no longer the saved one (colour, name or the banner itself) loses its decoration and its entry (MapItemSavedData.java:401).
A map used on a placed banner did nothing. MapItem.useOn toggles that banner's marker through toggleBanner: SUCCESS swings and counts as a use, FAIL (the map refuses) ends the use quietly, and the item is never placed or used otherwise (MapItem.java:336).
A map could not add or drop a banner marker on request. MapItemSavedData.toggleBanner works only for a banner inside the map: one whose marker stands as it is loses it, otherwise the marker is added (renewed when the banner changed) unless the map already tracks more than 256 markers (MapItemSavedData.java:371, MapDecorationType.trackCount).
A map had no way to ask what banner stands at a position. MapBanner.fromWorld returns the banner block entity's base colour (the block's dye colour) and custom name, or nothing when no banner is there (MapBanner.java:25).
A map kept its banners and frames lists as raw tags, so the markers never showed. MapItemSavedData's constructor adds a banner marker in the banner's dye colour (MapBanner.getDecoration, the 16 banner decoration types) and a frame marker for each saved entry, without a level, and the lists are written back (MapBanner.CODEC, MapFrame.CODEC, MapItemSavedData.java:100). A locked copy keeps the banners and the decorations (MapItemSavedData.locked).
A map kept one marker per player and rebuilt the item ones, with no place for a banner or frame marker. MapItemSavedData.addDecoration files each MapDecoration under a key (player, banner, frame), keeps its place in the drawing order, clamps it to the map, swaps a player's arrow for the off-map ones and drops what falls outside (MapItemSavedData.java:253). Markers now carry their name and key, and the update packet sends the name.
With two or more matching offers a villager swapped nothing: the held item never changed. Vanilla counts 40 ticks, then holds up the next display item and wraps to the first (ShowTradesToPlayer.displayCyclingItems); start resets the counter and index.
A villager looking at a player who holds a trade's cost now collects the offers in stock that take that item, holds up the first and keeps looking for 900 ticks instead of 40 (ShowTradesToPlayer.findItemsToDisplay, updateDisplayItems, displayFirstItem). The held stack keeps its count and enchantments and never drops (displayAsHeldItem). Offers not made yet are rolled by the level first.
ShowTradesToPlayer clears the main hand every tick it has nothing to show and again when it stops, restoring the default 0.085 drop chance (ShowTradesToPlayer.clearHeldItem). Horizon left whatever the villager held in place.
A brain tick holds only the mob, but AbstractVillager.getOffers makes the trades the first time they are asked for and that needs the level's trade data and random sequences. VillagerOffers has the game run ensure_offers for the villager, as the trading screen does.
SetEquipment carried only the main hand's item and count, so an enchanted item a mob holds (the offer a villager shows) never glinted on a client. Vanilla sends the whole stack (ClientboundSetEquipmentPacket); the server now adds the held enchantments from the gear table and the client keeps them there.
A villager's held item was never drawn: its model has no hand arms, so ItemInHandLayer found no hand. Vanilla's VillagerRenderer adds CrossedArmsItemLayer, which puts the main hand's item on the arms and shows it in the GROUND display context (CrossedArmsItemLayer.java), as the witch already does.
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]>
Select the nearest listener in each world, suppress duplicate world sounds, and clean up shared listeners and channels. Inaudible sources continue advancing. Part 2 of the splitscreen series; game integration follows separately.
Co-Authored-By: Codex <[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]>
--version-json, --client-jar, --libraries-dir and --assets-dir name
the files a launcher keeps in a layout of its own (instance folders,
shared libraries and assets), each replacing only what --minecraft-dir
would give; every file is still checked against research-lock.json's
pins. GameFiles gains the libraries and asset store folders, and
Options the Inputs (Default in the launcher, so it is unchanged).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--complete prepares, copies the sounds, then fails unless --check would
pass: one command for resources with nothing missing (without the sounds
step the game is silent, which is easy to miss).
--progress json prints one JSON object a line: {"message": ...} for each
log line, {"fraction": F} as each of the preparation's 18 stages begins
and at the end, {"error": ...} on failure. prepare_with_progress takes
the fraction; prepare keeps its signature, and a test keeps STAGES equal
to the stages steps() opens.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--output DIR puts the prepared resources elsewhere than
<root>/.local/horizon-client/<version>, for preparing and for the asset
commands (sounds, panorama, fonts, packs, languages, now one add_assets
behind them); the exporters' scratch files stay in <root>/.local.
--check exits 0 when the resources are complete and current for this
preparer and 1 otherwise, printing each gap with the command that fills
it: the manifest's schema and Minecraft version, the OUTPUT_VERSION that
made them, and the extras the launcher completes (packs, panorama,
Unifont, languages, sounds). A preparation now stamps its resources with
its OUTPUT_VERSION (horizon-prepare.json), which the output digest
leaves out, so the stamp needs no new OUTPUT_VERSION.
Co-Authored-By: Claude Opus 5.5 <[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 LOD Distance button becomes a slider from 64 chunks to 4194304 (the whole world). Far terrain plans up to 22 levels with larger cells past 1024 chunks, meshes tiles from level 6 up as heightmaps, makes a very wide face one scaled quad instead of thousands, and bounds the clip subdivision of coarse tiles around full-detail chunks.
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
991 changed files with 218198 additions and 10507 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.'
Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, and the one rule that shapes
everything is that **it plays exactly like vanilla**. This page is the short version of how we work; questions are
welcome on [Discord](https://discord.gg/HcTeMZxe5E).
welcome on [Discord](https://discord.gg/playhorizon).
This repository holds the game and the launcher, plus the tools, tests and docs that build, check and release them.
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything else (videos, Discord and other community material) lives elsewhere.
@@ -113,6 +113,10 @@ it (edited on every push, never repeated): talk to its author in the thread befo
can see.
- **Tick "Allow edits by maintainers".** When something small is left (a lint, a rebase, a nit) we push the fixup
ourselves instead of sending it back to you; only design questions come back.
- **Once we've pushed to your branch, merge main into it; don't rebase or force-push.** A rebase or force-push
replaces our commits with yours and erases the fixes we added (it happened on #5188 and #5203). Tell your agent
too: `git merge origin/main`, never `git rebase` or `git push --force` on a branch a maintainer has touched. If
our fixes get erased anyway, we finish the work in our own pull request that keeps your commits and credits you.
- **Tick its kind** in the template: `parity` (Horizon now does what vanilla does), `bug` (a crash or breakage),
`perf`, `feature` (new for players), `extra` (Horizon-only, off by default), `tooling` or `docs`. A workflow sets
that label, and area labels (`launcher`, `mobs`, `worldgen`, `rendering`...) from the paths you change.
@@ -136,5 +140,5 @@ new pushes, we add it again.
## Talk to us
[Discord](https://discord.gg/HcTeMZxe5E) for questions and ideas, [issues](https://github.com/horizon-rust/horizon/issues)
[Discord](https://discord.gg/playhorizon) for questions and ideas, [issues](https://github.com/horizon-rust/horizon/issues)
for bugs. To take something on, `/claim` its issue ([above](#claim-before-you-start)).
@@ -111,7 +111,7 @@ What is in progress, what comes next and where to help: [ROADMAP.md](ROADMAP.md)
## Contributing
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/HcTeMZxe5E). Before a pull request, run the self-check and paste its output:
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
@@ -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 991 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.