🤖 Agent: WebAssembly removal 2/4 (crates: runtime, SDK, official WebAssembly mods), on #3350, merged up to date with main c7065027b. Workspace compiles (--all-targets --locked). Merge after #3350.
🤖 Agent: Ludovic's AI agent (Claude), triage. Docs only: a server.properties → server.toml page for owners, linked from docs/README. verify is green. Queued.
🤖 Agent: A Java instance's Home no longer shows Horizon's version picker or nightly updates; its chip names its own version and loader. Checked in the real launcher (scratch homes, Fabric Java instance and Horizon instance), launcher fmt/clippy/278 tests pass on this head.
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: `npx` is resolved by path (`npx.cmd` on Windows), with a clear error when it's missing. verify is green. Queued.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Thanks for the fix: with the wither held in place the scenario now matches vanilla. On this head (f5de793b3) merged into current main, the harnesses this change can reach pass: `command_exec`'s wither spawn (4/4), wither breaks (9/9) and ender dragon (5/5) groups match vanilla. Its 2 remaining mismatches are `/data get` printing a `custom_name` as a component; they come from main and this PR doesn't touch them. fmt, clippy and the horizon-client tests pass too. Queued for merge.
Home drew Horizon's version picker ("No version" or a nightly) beside a Java instance's Play,
and offered Horizon's next nightly in its hero. A Java instance now shows neither: its chip
says its Minecraft version and loader ("Minecraft 1.21.1 · Fabric 0.16.9").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The third-person model dropped its interpolation whenever it moved more
than 10 blocks in a tick, while the camera kept lerping from the old
position. Levitation 254/255 rises about 12 blocks a tick, so the body
sat a whole tick ahead of the camera and left the frame.
The model now lerps from the player's own old position, as vanilla's
EntityRenderer.extractRenderState does from Entity.xOld/yOld/zOld, the
same one Camera.setup uses. Teleports and respawns already reset that
old position, so they still jump without a sweep.
Fixes#3616
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. Removes `ctov.rs`'s modular_compat code, which nothing calls any more (lithostitched owns that processor), with no change in behaviour. verify is green. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Main gained another mob_ai case beside yours, so I merged main into your branch (no rebase). The only conflict was the new cases' place in `tools/test_mob_ai_parity.py`, and both are kept. Requeued at the merged head.
ArmorStandItem.useOn returns SUCCESS once the stand fits (FAIL on a bottom
face or when something is in its box), and SUCCESS swings the hand. The
local placement now swings, and a server's client runs the same check in
startPrediction, before UseItemOn goes out, and swings when it holds.
armor_stand_spawn holds the checks both share.
Fixes#4024
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.canStartSprinting refuses while isSlowDueToUsingItem (an item in
use whose use_effects do not set can_sprint), and LocalPlayer.aiStep zeroes
the double-tap window while it holds. The sprint phase had the condition but
the player never fed it, so the sprint key or a double-tap of forward could
start a sprint with a shield up, which vanilla does not allow (measured: 0.055
blocks per tick against 0.043 for the shielded walk). Items now carry
use_effects.can_sprint (spears set it) and the window passes the state to the
sprint phase.
Fixes#3422
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
The owner's sit toggle on tamed wolves and cats only stood aside for food
an adult could breed with (age 0, not in love), so fish or meat on a tamed
baby flipped it between sitting and standing and it never grew.
Wolf.mobInteract and Cat.mobInteract call TamableAnimal/Animal.mobInteract
first and toggle setOrderedToSit only when that did not consume the click;
Animal.mobInteract takes the food from an adult that canFallInLove at age 0
or from a baby that canAgeUp (AgeableMob.canAgeUp: isBaby and not
isAgeLocked), growing it by getSpeedUpSecondsWhenFeeding.
mobs::ai::takes_food is that test, shared by the sit guards and
feed_animal (which now also leaves age-locked babies alone, as canAgeUp
does).
Part of #2780.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderDragon.knockBack pushes the server's copy of every player in a wing's
zone each tick, but a player's own client only receives its velocity when a
hurt marks it (LivingEntity.hurtServer's markHurt, sent by
ServerEntity.sendChanges). Horizon added the push straight to the player's
velocity on every tick they stayed in the zone, perched dragon or not, so
players were flung several blocks per tick around the exit portal.
The push now goes out only with a 5-damage wing hit that lands outside the
hurt cooldown: never under a perched dragon (isSitting), and not within 2
ticks of a mob's hurt (getLastHurtByMobTimestamp() < tickCount - 2). The
vertical push is 0.2f widened, as in vanilla.
Fixes#2893
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BegGoal sets Wolf.DATA_INTERESTED_ID on the server, but the LAN host
never sent entry 20 and a client never read it, so every window
(single player joins its server thread the same way) kept the head
straight. Send the flag with its default, read it into the remote
wolf, and run the part of Wolf.tick both sides run on the client:
interestedAngle eases 0.4 of the way toward the flag each tick, which
getHeadRollAngle turns into the head's roll.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
head_item_geometry now draws the local player's and other players' head
items; main's player_pumpkin_geometry stays only for LAN guests drawn on
the in-frame host, whose models carry no worn stacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. `travelInWater` (the 0.8 slowdown, then `getFluidFallingAdjustedMovement` with gravity/16) and the bubble column push for armour stands match vanilla. The per-tick water scan only covers the stand's own box. CI parity is green at this head. Queued.
Fixes#3531's render side landed in #3341; this adds the shadow cases its
branch carried for tools/test_text_display_render_parity.py so the
packed ARGB ints, the [r, g, b, a] lists and NO_SHADOW=0 are checked
against the vanilla TextDisplayRenderer oracle.
🤖 Agent: Ludovic's AI agent (Claude), triage. `WaterAnimal` and `AgeableWaterCreature` set the WATER malus to 0, and FishMoveControl's yaw uses `Mth.atan2` in degrees. CI parity (mob_ai with new fish cases) is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. `WitherBoss.hurtServer` ignores damage whose source entity is in `#wither_friends` (the undead), for explosions and wither skulls alike. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Sleeping now follows the sky darkening (`skyDarken >= 4`, `isDarkOutside`) instead of a fixed time window or the thunder flag alone, so a thunderstorm at noon allows it and rain alone doesn't. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. `MinecartFurnace.push` is a Vec3, and `calculateNewPushAlong` (`projectedOn(...).normalize().scale(push.length())`) keeps its 3D length; saving drops y as `addAdditionalSaveData` does. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Matches `StructureTemplate.placeInWorld`, which sets a barrier wherever saved data goes, so the old block entity always goes and the template's data always loads. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Read the whole diff: a loaded chunk's block entity replacing one at the same position keeps its ticker slot only if the new state ticks, as `LevelChunk.setBlockEntity` + `updateBlockEntityTicker` do. CI parity is green at this head. Queued.
🤖 Agent: Redesign R2 (the animated night sky behind every page), stacked on #3636 which merged; approved by Ludovic 2026-10-29 (8 px/s clouds accepted); launcher fmt/clippy/tests and sky.rs tests pass on this head, CI green.
ClientLevel.tickWeatherEffects reads the MOTION_BLOCKING heightmap for
each splash column, but the client scanned down from 64 blocks above the
camera, so under a roof higher than that (a deep excavation) it found
the floor and made rain particles and the rain sound there. The splash
columns, the sound's roof check and the animate tick's isRainingAt now
read the chunk heightmap that the rain columns already use.
Fixes#3810
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Redesign R1b (the night look's specimen page, pixel-cut plates in plate.rs), stacked on #3635 which merged; approved by Ludovic 2026-10-29; launcher fmt/clippy/tests and plate.rs tests pass on this head, CI green.
🤖 Agent: Redesign R1 (night tokens, Bricolage with Geist's Cyrillic, pixel-cut corners), approved by Ludovic on 2026-10-29; launcher fmt/clippy/278 tests and horizon-theme tests pass on this head, CI green.
LivingEntity.setPosToBed puts a sleeper at the bed's x/z centre and at
pos.y + getSleepHeight + 0.125, getSleepHeight being the top of the bed's
shape (0.5625). Horizon left out the 0.125, so the player and sleeping
villagers lay 0.125 lower, and the first-person camera (eye 0.2 + 0.15)
ended up inside the player's own head.
Fixes#4006
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The item billboard of eggs, snowballs, ender pearls, fireballs and llama
spit took its right axis from forward x Y. At pitch exactly ±90 forward is
vertical; f32 cos(90°) is slightly negative, so the axis flipped sign and
the sprite turned upside down. It also ignored third-person front view.
ThrownItemRenderer.submit rotates the item by CameraRenderState.orientation
(Camera.setRotation: rotationYXZ(π - yaw, -pitch, 0), turned round in front
view). Use the same rotation: camera_orientation's math moves to a free
camera_rotation so the billboard's axes come from it.
Part of #3393
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-1: read the new head. It only merges main and replaces the RD 8 and RD 32 reports with reruns on an idle machine, so the write-up no longer leans on runs with builds alongside. tests/test_perf_bench.py and the public tree check pass. skip-release stays. Thank you!
The scrollers on the stonecutter and merchant screens only moved with the
mouse wheel: pressing and dragging them did nothing. Port
StonecutterScreen.mouseClicked/mouseDragged and
MerchantScreen.mouseClicked/mouseDragged, and keep the stonecutter's
scrollOffs as a float like vanilla so the wheel and the scroller share it.
Part of #3518.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
The wither checks (WitherSkullBlock.checkSpawn and the dispenser's
canSpawnMob) only tried upright T builds with the arms along X or Z.
Vanilla's BlockPattern.find tries all 24 forwards/up pairs from every
corner in the cube at the placed block, so flat, upside-down and
sideways withers build there.
Port BlockPattern (find, matches, translateAndRotate) to
horizon_client::block_pattern with WitherSkullBlock's full and base
patterns, and use it for checkSpawn and canSpawnMob. The wither now
takes its yaw from the match's forwards axis (main had it inverted for
upright builds), its base blocks from #minecraft:wither_summon_base_blocks,
and its summoned_entity trigger from the 50-block box, sharing the
golems' box check.
Fixes#3098.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths; tipped/spectral arrows and text/block/item displays drawn alike by host and LAN guest, save and reload identical, the ominous item spawner seen by a LAN guest. Mojang rules, supply chain, per-tick cost read.
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): 6da84cf19 was proven on union/pt3-w2; this head only merges main and adapts main's two new held-item tests to the Tag custom name. Workspace check, 1508 tests and the 11 item/save harnesses pass on it.
The 128 atlas skin slots went round every frame once more skins were in
use than slots: update_remote_skins touched every player-list entry's skin,
each frame's uploads pushed out the least recently shown one (even one drawn
the frame before), and that one came back the next frame by pushing out
another. Players vanished for a frame (their name tag stayed) and heads
swapped to the default skin and back.
- skins::SkinSlots holds the slots: a new skin takes a free slot or the
least recently drawn one, never one drawn in the last frame; with every
slot drawn it waits and its player keeps the default skin.
- Downloader::drawn records what each frame draws (level players and their
capes, the local player, mannequins, the faces the tab list and spectator
menu show, skull blocks, item and text heads).
- Only drawn skins ask for a slot, not every player-list entry, and a skin
not in the atlas yet draws the default one (SkinManager.createLookup's
getNow(DefaultPlayerSkin.get)) instead of a missing texture.
Fixes#3829
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by Ludovic's AI agent (pr-triage-2, Claude) at c566e69c6, merged with current main: the End gateway follows EndGatewayBlock.entityInside and Entity.handlePortal; the latest commit puts back the tick_items call that the first one had dropped (dropped items fall, merge and despawn again) and, as Entity.setAsInsidePortal does, only restarts the portal cooldown of an entity that is still cooling down (checked against the 26.3 bytecode). clippy -D warnings, the portal/gateway/item unit tests and tools/singleplayer_end_cargo_audit.py on both paths pass (cow and a 7-diamond stack arrive at [1035.5, 76.0, 1.5]). Thanks @levzzz5154.
🤖 Agent: verified by Ludovic's AI agent (Claude): the review point is addressed (decorated protos still save as minecraft:features); the forced ticket rings follow FORCED_TICKET_LEVEL 31 rising one per ring; merged with main, clippy -D warnings and the horizon-client tests (forced_chunks included) pass. Queued; the merger waits for CI parity.
🤖 Agent: pr-triage-2 checked the pin: 3f99a5c60 is horizon-rust/horizon-mixins main, 8 commits ahead of 0d6b103c7 (preserved standard macros, Windows verbatim output paths), nothing behind; only MIXINS_REV changes and explicit per-bundle pins still win. Thanks @levzzz5154.
plate.rs draws a plate as rectangles that don't overlap (one opacity, the 1 px stroke follows
the steps); the specimen compares it with the rounded controls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Plates cut their corners in two 4 px steps (PIXEL_CORNER); RADIUS_PLATE is gone
from the night tokens. tools/bake_bricolage.py now merges Geist's Cyrillic into
every cut, so Russian uses the launcher's own face.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c32dddb1f: carried and worn blocks draw their block-state model with vanilla's per-face default tints (block_tint harness against 26.3), grass colour once per draw; parity --changed passes, clippy clean, tests pass but for main's own glowing test. Merging.
🤖 Agent: verified by server-tick-1: proto light views built once per proto state (streaming span 10-24 → 3-4 ms/tick on 25 explore bots, RSS unchanged); new generation_saves test fails on main and passes here; pr_ready --quick green; merging once CI parity passes.
🤖 Agent: pr-triage-1 merged main once more (fast-forward): your Polish checks sit beside main's new test that every key has a Russian value. horizon-client-core's lang tests, the launcher's i18n tests and rustfmt pass on this head; proven before on union/pt1-w4.
The darkness effect's blend (MobEffectInstance.BlendState, which the
lightmap and the fog read) was ticked only by tick_sculk_entities, which
runs in the integrated level's tick_world. A server's client (multiplayer
and the default single player, whose server runs on a thread) ticks its
effects in tick_client_effects instead, so the blend stayed at 0 and the
effect did nothing. tick_client_effects now ticks the blend after counting
the effects down, as MobEffectInstance.tickClient does.
A new darkness from ClientboundUpdateMobEffectPacket starts its fade from
nothing, or at full strength when the server sent it unblended (joining,
changing dimension), as MobEffectInstance.skipBlending does.
Part of #3393.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two tests main added (entity_nbt, mobs) set custom_name to a string and compared custom_name() with Option<&str>; with this PR it is the component tag and the plain text is an owned String.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths, and the throws levzzz asked for (walking/jumping carry the movement, none after a tick without a move) for host and guest on both paths. Mojang rules, supply chain, per-tick cost (a Vec3 copy per guest turn) read.
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1. The affected vanilla harnesses, clippy -D warnings and the tests pass, and so does lan_e2e (vehicles, commands, armour, self, tab) on both single-player paths. lan_e2e's minecarts check fails on main's own code too, with or without this PR. Furnace carts now clear their lit flag for clients, and their smoke follows MinecartFurnace.tick. Thank you!
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): launcher-only change; on this exact head launcher fmt, clippy -D warnings and 299 tests pass on the Mac mini, clean merge with main. Asset refill only through download_for_owner (owners only).
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1: the affected vanilla harnesses pass (the only failures are main's own or another PR's), clippy -D warnings and the tests pass, the launcher's checks pass, and lan_e2e passes vehicles, commands, armour, self and tab on both single-player paths. Thank you!
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at d8e912738 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a717d6d58 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1ffe68635 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
CarvedPumpkinBlock.trySpawnGolem uses BlockPattern.find, which tries every
corner in the box from the pumpkin and every forwards/up pair, so the snow
column and the iron T also count lying down or upside down. Horizon only
checked the upright column and the upright T with arms along X or Z.
find_pattern follows find and translateAndRotate; the cells and the spawn
base come from the match (getBlock(0, 2) for snow, getBlock(1, 2) for iron).
Fixes#3929
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 217216e26 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 825f8f884 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c455bb6ec in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a11e54e7b in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c7aca8489 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
LivingEntityRenderer.getShadowRadius multiplies the type's radius by
minecraft:scale, and MobRenderer by the age scale (babies 0.5, or the
species' own: goat 0.55, camel 0.6...). Villagers and striders halve a
baby's shadow again, turtles take 0.83 of it; slimes, magma cubes,
sulfur cubes and pufferfish size theirs by their size or puff state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 95c234cc1 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 0771f59bc in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 52cc53a4f in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
Main's #3401 drew blocks through tinted_block_model_vertices with a per-face
tint closure; block_model_vertices now passes the default tints there
(grass colour once per call). The enderman's carried block uses the default
tints, so block_model_vertices_tinted has no caller left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1a01f5ef5 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
A client of a server has no live biome features, so the behaviours'
isRainingAt answered false and a Riptide trident never started charging in
rain.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: merging the short roadmap (#3859 step 4): verify green, roadmap tests and the online check pass. Proposed owners can change their line with a one-line PR.
Kira's Discord /claim marks an issue with the `in progress` label instead of
assigning it; `/claim` on GitHub now refuses such an issue, so the two
claim paths can't hand the same issue to two people. Kira's side (refusing a
Discord claim on a GitHub-assigned issue) is in horizon-rust/kira.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
roadmap.toml items get `owner` (a GitHub login) and `issue` (the tracking
issue); `check` requires both on every Now item, at most 8 Now and 12 Next
items, and the tracking issue to be an open issue. ROADMAP.md shows them,
roadmap.json carries them (the hub ignores unknown keys).
The proposed lists, from the work actually happening (open pull requests,
agents' workstreams):
- Now: smooth-frames, server-tick, mods, port-ledger (the player-visible
gaps), modpack-worldgen, server-thread, java-instances, lean-code, with
tracking issues #3909-#3916.
- Mods: one line (Ludovic's decision), owner levzzz5154; native-mods,
mod-registry, server-mods, mod-screens, modder-tools, machines-api and
mod-world-items fold into it.
- Shipped (`sync`, all their pull requests in a release): server-dialogs,
discord-presence, native-shaders, pregen, item-cooldowns, chat-settings,
pointer-hand, text-field-keys, chicken-flap. phones-and-browser is not
shipped: it becomes ongoing so `sync` stops taking its first pieces for it.
- Next: 12 (splitscreen, owner martipops, and launcher-redesign come up);
the rest of Next moves to Later.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleUpdateAttributes gives every living entity its
attributes; the client kept only a mob's max health, so a server's scale
on another player or a mannequin was dropped and NPC plugins' enlarged
fake players showed at their normal size. The scale now goes to the
model as the local player's already does (LivingEntityRenderState.scale),
computed like AttributeInstance.calculateValue, now a function with a test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Component.tryCollapseToString: ComponentSerialization.CODEC writes plain text with no style and no siblings as a bare string, so a legacy display Name '{"text":"Cool"}' becomes custom_name:"Cool". Keeping the compound made 215 of hotbar's 905 cases differ.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemStackTemplate.CODEC bounds the count with intRange(1, 99). Crafting,
cooking and stonecutting results were cast with 'as u32', so 0, 2^31 and
wrapped 2^32 reached item stacks. Such recipes are now not loaded.
🤖 Agent: merging the kind and area labels (#3859 step 3): verify green, workflow-safety and labelling tests pass; labels created and the open pull requests labelled once.
On open and push, overlap-note.yml (pull_request_target, main's script only)
lists, in one comment edited on every push, the other open pull requests that
close the same issue or change the same functions. Functions come from git's
own rust/python hunk headers, which name indented methods (GitHub's stop at
`impl Game<'_>`). The heads are fetched as objects and diffed, never checked
out; hooks, fsmonitor, external diff and textconv are off.
Measured on the 207 open pull requests: one shared function, or a few of a
big pull request's hundred, was mostly chance, so a pair needs a shared
issue, or 2 shared functions and a tenth of the bigger one's; functions 5
compared pull requests change, probes, tests and stacks don't count.
21 pairs (34 notes): 16 related (4 duplicates), 4 doubtful, 1 unrelated.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ModelEngine names a bone's item model `modelengine:<model>/<bone>` and
ships it at assets/modelengine/items/<model>/<bone>.json. Only the files
at the top of assets/<ns>/items were read, so every bone stack resolved
to the built-in missing model, whose sprite is absent from the HUD
atlas: the quads were dropped and the NPC rendered nothing while its
interaction hitbox stayed clickable.
Walk the tree like ParsedModels::load already does for models.
Co-Authored-By: Qoder <[email protected]>
effect_modifiers only mapped strength and speed, so Weakness and Slowness
had no effect on a mob: its melee damage and movement speed stayed at the
base values. Map every attribute-carrying effect the way vanilla's
MobEffect.addAttributeModifier does, scaled by amplifier + 1:
- weakness: attack damage -4 per level (ADD_VALUE)
- slowness: movement speed -0.15 per level (ADD_MULTIPLIED_TOTAL)
- health_boost: max health +4 per level (ADD_VALUE)
Move the per-effect table into a testable effect_modifier() and cover all
five rows with a unit test.
Fixes#3887.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
issue() wrote the body file in the system's code page, while gh reads
it as UTF-8, so on Windows a body with "→" raised UnicodeEncodeError
(cp1251 here) or would reach GitHub garbled. #3587 described this fix,
but the merged pull request kept only its roadmap files.
test_apply_lists_the_new_issue_numbers_in_the_tracking_issue now passes
on Windows without PYTHONUTF8.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On macOS and Windows nothing calls it, so clippy -D warnings failed there
(dead code) while Linux CI passed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by server-tick-1: due scheduled ticks run in one with_level while a step leaves no level events or spill (identical order and state); tick_blocks span 15.4/14.8 → 10.4/9.3 ms at 50 bots; pr_ready --quick green; merging once CI parity passes.
On the server thread (the default) breeding runs in the mob tick, outside
any player's turn, and the server has no host player, so the
bred_animals trigger and the animals_bred statistic were dropped and
"The Parrots and the Bats" was never granted.
Remember who fed each animal into love (Animal.loveCause) and credit that
player, the parent's feeder first and else the partner's, as vanilla does.
F2 was handled in `Game::key_pressed`, which a screen with a text box never
reaches: chat, a sign, a book, the command block and the search boxes take the
key first, so F2 did nothing there (#3689).
Vanilla runs `Minecraft.handleGlobalKeyPress` before a screen sees the key:
`key.fullscreen` and `key.screenshot` are window shortcuts that work on every
screen, and a held debug modifier suppresses both. The fullscreen half was
already global here; this adds the screenshot half beside it, driven by the
`key.screenshot` mapping instead of a hard-coded F2, and drops the old arm in
`key_pressed`. `horizon-ctl key` takes the same shortcut, so an agent drives
what a player does.
Co-Authored-By: DeepSeek V4.1 Flash (opencode) <[email protected]>
The template gets a "Kind" list (parity, bug, perf, feature, extra, tooling,
docs). pr-labels.yml (pull_request_target, main's script only, never the
pull request's code) sets the ticked kind when the pull request has none, swaps
it when the author changes the tick, and never removes a label set by hand.
Area labels come from the changed paths: at most 3, each a tenth of the lines.
`feature` keeps its release meaning.
pr_labels.py also creates the labels and labels the open pull requests once
(backfill: kind only where clear, else none).
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In vanilla Minecraft (26.3), exiting or disconnecting from a remote server
(`isLocalServer() == false`) never shows the saving world screen
(`GenericMessageScreen(Gui.SAVING_LEVEL)`). In `disconnect()`, vanilla only
halts the integrated server and displays the saving level screen when
`singleplayerServer != null`.
Previously in Horizon, `begin_closing_save(quit = true)` set `Stage::Writes`
when quitting from a remote server or the title screen, causing
`render_closing_save` to draw "Saving world...", and `save_for_exit` called
`save_game()` on exit even for remote worlds.
- In `begin_closing_save`: introduce `Stage::Quit` for clean termination
without drawing the save progress screen. When quitting while connected
to a remote server, immediately disconnect the session, mark
`saved_for_exit`, and transition directly to `Stage::Quit`. Quitting
from the title screen also uses `Stage::Quit`.
- In `close_world`: directly call `leave_server(None)` when on an external
server (`self.external_server()`), skipping `begin_closing_save`.
- In `save_for_exit`: cleanly disconnect `remote` and return without saving
when on an external server.
Co-Authored-By: Antigravity <[email protected]>
Anyone comments `/claim` on an issue to be assigned (GitHub lets an issue's
commenters be assigned, no role needed); an assigned issue is taken.
`/unclaim` gives it back; the trusted reviewers can `/unclaim @login`.
A daily run releases claims this workflow made over 5 days ago when no open
pull request links the issue; hand assignments are never released.
Same shape as triage-command: main's copy runs, no pull request is checked
out, the comment reaches the script as env vars, the token writes issues only.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by pr-triage-3. Dev tooling only (tools/port_ledger.py harness coverage + gaps --kind untested, docs, port_map paths moved to the split crates: every new path exists on main). Merges cleanly with main; tests/test_port* 30 pass; skip-release.
The scenario depended on the wither staying put for the 20 ticks before
destroyBlocksTick fires. The wither's WaterAvoidingRandomFlyingGoal can
set off on a stroll in that time (its random is unseeded in vanilla),
which shifts the 3x3x4 box it breaks, so the stone at 7 210 7 was broken
or left depending on the run, on either side.
Teleport it back to 8.5 210 8.5 every tick until then: customServerAiStep
runs before travel, so the break always sees that position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A retracting piston is a moving_piston block for two ticks. The support
facts are exported without a block entity, so the block read as empty and
every face as unsupported, and the lever attached to the piston's back
popped off. Read the extended base for a retracting source piston, which
is what vanilla's moving-piston block entity shape contains.
🤖 Agent: proven on union/pt1-w3 by pr-triage-1: every vanilla harness this branch reaches passes (mob_ai and the equine ones included), clippy -D warnings and the horizon-client tests pass (the one failing test is main's own, fixed by #3851), and the last commit only reorders an import for rustfmt. Thank you!
ludovic111 pointed out that the first pass's RD 8 and RD 32 games had builds
running alongside, which docs/benchmark.md keeps out of published results.
Each game now started only with no compiler or other game running and the
CPU under 15 % for 30 s, and a game during which one started (or other
processes used over 15 % for 10 s) was run again. RD 16 was already idle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The glyph loop takes both: an inline object draws and skips the shadow passes; other glyphs use the component's shadow_color. Both sides' tests kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. Block light follows vanilla BlockLightEngine.checkNode (an emitter in its own light only brightens): in-frame relight during the RD 32 ocean flight 17–30 ms per run down to 7–8 ms; light harness and the light tests pass; CI verify green.
ArmorStandRenderer adds WingsLayer after the armour layers, but
armor_stand_geometry never drew the chest slot's wings. Split the
wings drawing out of player_elytra_geometry into wings_geometry and
call it for stands, at the resting wing angles (a stand never glides
or crouches) and with the baby elytra model for a small stand.
Mob::add_effect normalizes ids to their minecraft: form (08b97652), but the
test asked has_effect for the bare "glowing", so the assertion failed.
Fixes#3840
Language::ALL holds eight and every list of codes names pl. Polish gets the
seven launcher strings main added since (instance trash, native mods, the
Java Edition line) and the build-time line main reworded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's BlockLightEngine.checkNode clears a changed block's light and
decreases from it only when the new emission is below the stored level;
otherwise the level stays, the neighbours' light is pulled in and the
emission increases from there. Horizon's relight cleared every changed
block, so each flowing lava block placed next to lava (fluids settling
in fresh chunks while flying over the ocean) darkened and relit up to
14 blocks around it on the render thread, and marked every section
there for remeshing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/ride a text_display mount @s makes the window's own player the
vehicle, which isn't in mobs: the display stayed where it spawned. The
local player seats it at the top of its box in its pose and scale, its
passenger attachment (Avatar.POSES), as for other players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: WebAssembly removal 1/4 (game), merged up to date with main c7065027b. The official mods are native in the registry (Zoom, FPS Counter, Keystrokes, Minimap, Recipes, Coordinates HUD 2.0.0, verified in the published index), and the authors of the WebAssembly mods with no native version were told (list in the body). Each layer compiles; pr_ready --quick on the tip: only main's own macOS clippy (log_display) and axolotl test fail. Parity: 340/340 locally on the previous heads; CI parity on these. Merge before #3351, #3353, #3371.
🤖 Agent: merging. The frame no longer waits on a descheduled worker's mpsc send: 8–24 ms stalls in Streaming::update gone in the RD 32 ocean flight (0–1 per run vs 5–14), 109/109 affected harnesses pass, CI verify green.
🤖 Agent: correction from pr-triage-1: my first push of the main merge missed one line (`Language::ALL` still said six), so it didn't build; 46ac49ad0 adds it and merges main once more. On this exact head the launcher's fmt, clippy -D warnings and all its tests pass, including the check that every language has every string. Sorry for the noise.
The game thread drained the workers' results with std's mpsc try_recv,
which spins and then yields until a sender it finds half way through a
send has written its message (std's mpmc list: start_recv, wait_write).
A worker descheduled at that point on a busy machine held the frame for
a scheduler quantum: 8 to 24 ms waits with no CPU time in
Streaming::update, measured with HORIZON_SPIKES during the RD 32 ocean
flight. The results now go through a mutex-guarded queue the frame
takes with try_lock: while a worker holds it, the frame leaves the
results for the next one. The blocking waits (save, wait, drop) keep
their timeouts on a condvar.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflict: the help text; main's --in-frame deprecation kept, --dev-mod still
takes content mods only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`tick_scheduled` built the level (`with_level`: the boxes of every cart,
arrow and guest, the fire-spreading players, the game rules, the ticket
levels) again for each due block and fluid tick. Run the due ticks in one
`with_level` while each leaves nothing for the game to apply between
them, level events or a spill; then the game handles those as before, so
every tick still sees what the ones before it did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: the creative screen's tooltip takes main's tab names
(draw_stack_tooltip_with) and this branch's TooltipFlag.isCreative for a
grid slot; horizon-protocol's item.rs keeps main's instrument holder and
this branch's painting_variant holder. The new wire test's Dynamic gets
main's dialog_tags by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolved zip.rs: main added a_name_falls_back_to_its_folder_entry where this
branch added its deflate tests; kept both.
Co-Authored-By: Qoder <[email protected]>
Conflict: main made chat_tree callable from command blocks, with the
WebAssembly mods for its argument; it stays callable, without them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: main's changes to mod_render.rs, server_mods.rs and the runtime
part of mods.rs sit in WebAssembly code this branch deletes. Kept main's
Event::NativeSettings (a native mod's page asks for its labels again) and
the bundle's wheel; dropped the WebAssembly ModSettings event, the mod
screen's wheel and its title-screen settings page (9c6325c1f).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
UI Brightness takes params[6]; the tier lift also skips pixels the hand or
HUD covers, as main's lift does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven on the integration branch union/pt1-w2 (23 PRs on main 89f949155) by pr-triage-1: tools/pr_ready.py green (clippy -D warnings, the touched crates' tests, launcher checks), the 126 affected vanilla harnesses pass, and lan_e2e passes 13 checks on both single-player paths. Thank you!
🤖 Agent: proven on the integration branch union/pt1-w2 (23 PRs on main 89f949155) by pr-triage-1: tools/pr_ready.py green (clippy -D warnings, the touched crates' tests, launcher checks), the 126 affected vanilla harnesses pass, and lan_e2e passes 13 checks on both single-player paths. Thank you!
#3410 made the command parse oracle, which also exports
command-registries.json during preparation, write "selected_entities". The
prepared output changed (digest 55f3f289…) but OUTPUT_VERSION stayed 1, so
every parity run since fails "the prepared output changed", and players
would keep resources prepared before it. #3410 was proven in a local union,
which skipped CI parity and with it this check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven on the integration branch union/pt1-w2 (23 PRs on main 89f949155) by pr-triage-1: tools/pr_ready.py green (clippy -D warnings, the touched crates' tests, launcher checks), the 126 affected vanilla harnesses pass, and lan_e2e passes 13 checks on both single-player paths. Thank you!
The Wayland watch was made whenever the surface offered HDR, so with
HDR off (the default) the renderer still did a blocking dispatch on
winit's connection at startup. It's now made when set_hdr turns HDR
output on and dropped when it turns off; the next display_changed
applies what it first reported.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Paper White and Peak keep their Auto step (0) on Graphics: HDR, where
#3203 moved them; Black Level and UI Brightness are saved, parsed and
passed to set_hdr beside the Auto nits. The docs paragraph keeps the
HDR screen, Auto, Black Level and UI Brightness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained Black Level (#3145) and #3176 adds UI Brightness; both
move from Display to Graphics: HDR with the rest, in main's order
(Black Level, Peak, Paper White, UI Brightness), and wait for the HDR
switch like the other sliders.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merging main into #3625 put Spanish and Ukrainian both in Language::ALL
but left its length at 5, so the launcher no longer builds on main
(E0308: expected an array with a size of 5, found one with a size of 6).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained HDR Black Level (#3145). Both settings are kept: the
params slot after `pq` is the black level and the next one is the UI
brightness; set_hdr takes both; options save hdrBlackLevel and
hdrUiBrightness; the UI Brightness row follows Paper White.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolves two conflicts:
- gfx.rs: main made padded_row/copy_frame free functions (copy_frame takes
the device and size) and dropped write_screenshot's COPY_SRC check for
the OpenGL readback target; take_readback now calls the free copy_frame
and write_screenshot keeps the ScreenshotRequest signature.
- remote.rs leave_server: keeps main's hud_disconnected (which resets the
boss overlay), overlay_time and item_activation reset, plus this branch's
world_icon = None.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added CapeLayer (mobs::render::cape) at the spot this branch adds render_layers;
both stay. CapeLayer draws only when !state.isInvisible, so the player's own cape is
left out under Invisibility too, in the world and the inventory.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep main's Component::Instrument beside this branch's StewEffects (enum,
test and item_wire_probe).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_core::geometry::trig::fdlibm_atan2 is the same e_atan2.c port, and it
passes every vector of atan2_matches_strictmath, so jmath calls it instead of
keeping a second copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both languages stay in Language; the translation test walks Language::ALL; the CLI's
help, its error and the docs now list es too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
graphics_ui.rs: main passes the settings to layout, content_height and scroll;
the held scrollbar's code and tests take them too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Windows npx is npx.cmd, and subprocess (CreateProcess) does not find
it by name: the script stopped with FileNotFoundError. Resolve it with
shutil.which, as the script already does for rea, and say what is
missing when Node.js isn't installed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The attribution rule gave Ludovic's exact line first, so contributors' agents
copied it: #3800 (rdbtCVS) opened as "Ludovic's AI agent", #3499 as "AI
agent (Codex), not by himself". The rule is now a template filled with the
login the PR is opened from; Ludovic's line stays as a labelled example. The
PR template carries the same line as a comment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The in-frame single-player path is unsupported and will be removed soon.
The game prints a deprecation warning when it is requested, the help text
and docs say never to use it, and the e2e drivers warn on their flags.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Main's graphics screen now takes the settings in layout and extra_rows, and a
slider press starts a drag (SliderDrag) that saves on release (#2711).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockGetter.traverseBlocks visits the block a ray starts in first, and
VoxelShape.clip hits a shape that contains the ray's start. pick_fluid
began stepping from the eyes' block without testing it, so under water
a bucket took the next source along the ray (the feet's, looking down)
and no air pocket could be made. Fixes#3787, #3233.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolves the conflict in game/remote.rs tick_remote: keeps both the
remote spawner tick and main's client_tick_beacons.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). ElytraOnPlayerSoundInstance.tick line for line (stop when removed or after 20 ticks without isFallFlying, volume clamp(speed/4), silent 20 ticks then a 20-tick fade-in, pitch 1+(v-0.8) above 0.8), started like LocalPlayer.onSyncedDataUpdated when gliding starts. #3227 (same port) closed as its duplicate. union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
A column at the edge of the loaded area lights against its decorated
neighbours past the area, so the streamer turns those proto chunks into
columns (`light_view`) on the tick thread. It built them again for every
light job and for every retry of a column still waiting on its last
neighbour: on a dedicated server with 50 bots, 59 220 views for 5 345
proto chunks. Keep each view until its proto is checked out, replaced or
removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. Horizon Official at the top of the launcher's Multiplayer list, once, sharing the game's marker (Ludovic, 2026-10-29). pr_ready --quick green (launcher 273 tests); verify and the launcher matrix green.
🤖 Agent: merging. Horizon Official at the top of Multiplayer, once (Ludovic, 2026-10-29). pr_ready --quick green (945 tests); verify green; parity runs in CI.
An armor stand used the air travel in water too and ignored bubble columns,
so one standing in a soul sand column never rose. LivingEntity.travel now has
its water branch (slow down 0.8, gravity / 16 through getFluidFallingAdjusted
Movement), and the blocks the stand is inside (a bubble column) push it
between the move and the friction, as Entity.move's checkInsideBlocks does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Cat and Ocelot.customServerAiStep set sprinting while the move control
runs at 1.33 (fleeing a player) and the crouching pose at 0.6 (tempted).
LivingEntity.setSprinting adds SPEED_MODIFIER_SPRINTING (+0.3F,
ADD_MULTIPLIED_TOTAL) to the movement speed, which Horizon only used for
the model pose, so a fleeing ocelot or stray cat walked 1.3x too slowly.
The step now runs between navigation and the move control, as in
Mob.serverAiStep, and sets Extra.sprinting and the pose. A sprinting mob
also gets the rest of vanilla's isSprinting: the speed modifier, the
sprint jump impulse, the 0.9 water slowdown, no fluid-falling gravity
and the two random draws of spawnSprintParticle. The server sends shared
flag 0x08, the CROUCHING pose and the sprinting modifier; the client
reads them, and the feline model takes its pose from them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An empty `biome_is` list on a surface that does not number its biomes
compiled to the id comparison, since collecting no indices never asks
`biome_index`. It then failed with MissingBiomeProvider where vanilla and
main answer false (material_condition's codec-corpus condition 49). It
keeps the name lookup main uses. Review on #3366.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The OpenGL backend's GLSL translator (naga) does not support `textureLoad`
from depth textures. When starting on OpenGL (such as on Linux with Intel
HD 4000 via Mesa crocus), `Post::new` panicked while compiling `fs_taa`
in `world_post.wgsl` (`WGSL textureLoad from depth textures is not
supported in GLSL`), crashing the game on startup.
Wrap TAA pipeline creation in an error scope, matching the animated
sprites compute pipeline fallback in `animate.rs`. When TAA cannot
compile, log a warning and disable TAA, letting the world post pass fall
back to FXAA or direct presentation without panicking.
Co-Authored-By: Antigravity <[email protected]>
Move the active colormap lookup outside the model/face loops, as requested in
review. All tinted faces in this draw share the same default grass colour.
Implemented by OpenAI Codex on behalf of NetuosoDEV.
Co-Authored-By: OpenAI Codex <[email protected]>
main's new item_sync test builds a Dynamic by hand; it takes the rest
(dialog_tags) from Default, as this branch's other tests do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by Ludovic's AI agent (Claude): follows CreateWorldScreen's temp data pack folder flow; main merged, clippy and the datapack/title/pack screen tests pass, verify green. Queued for the merger.
NPC plugins hang a text display on a fake player with SetPassengers, and
vanilla seats it every tick (Entity.rideTick, positionRider) at the
vehicle's passenger attachment, the top of its box, less the display's
own vehicle attachment, which is 0. remote_set_passengers placed only
mobs and the local player, so a ridden display stayed where it spawned,
at the NPC's feet (tensa.co.ua's lobby: the glyph titles under the NPCs).
A remote text display now keeps its vehicle and position_riders seats
it after the tick, from the last tick's seat, so it follows a moving
vehicle smoothly; a teleport sent meanwhile no longer drags it off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecartFurnace.calculateNewPushAlong projects the push onto the
movement and scales it back to the push's own 3D length. Horizon kept
the push as x/z only and normalized with the vertical part, so the
push lost length every tick while gravity gave the movement a vertical
part (the cart moved with a 0.04 per tick fall). A cart fuelled from
about half a block away slowed down to about 0.56 to 0.79 of vanilla's
distance over its 3 minutes of coal.
Keep the push as a Vec3 between ticks. Saving still writes only PushX
and PushZ, and loading and fuelling set y to 0, as vanilla does.
Vanilla: MinecartFurnace.applyNaturalSlowdown, calculateNewPushAlong,
addFuel, addAdditionalSaveData, readAdditionalSaveData;
Vec3.projectedOn, Vec3.normalize.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
/place template wrote a block's saved data over whatever block entity
was at its position and dropped the old one without removal handling.
Over the same block state, place_state returns early, so a playing
jukebox kept its song and an open container screen stayed on the
replaced entity.
StructureTemplate.placeInWorld first sets a barrier where a block with
saved data goes, which makes LevelChunk.setBlockState remove the entity
there. Do the same before placing: remove the entity and its custom name,
stop a removed jukebox (place_state does that itself when the block's
name changes) and close a container screen open on it.
Refs #3708
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
brush.rs: main's player_generates_container_loot trigger (#33c15a6d7) is
kept in unpack_brush_loot, which now reads the Brushable block entity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#3253 (on main) replaced BlockSection's local_ids map with a scan of the
live palette and a last-lookup cache. fill_column now finds the state's id
with id_in, clears the cache when the old state's slot is freed, and sets
it when the section becomes uniform.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Multiplayer section puts "Horizon Official" (horizon.bunchy7s.dev) first in the instance's
servers.dat, as the game does: once (horizon_official_server.txt beside the list, the same file the
game writes), never again after the player removes it, never when the list already has its address
(any case, with or without :25565), and never into a list that can't be read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntityRenderer.submit scales the whole pose stack by the entity's
scale. Horizon scaled the player's eye height, box and camera distance but
drew the model at scale 1, so a player with scale 2 looked half its size
from the pulled-back camera. The local player's model, and what hangs off it
(arms, held items, cape, elytra, armour), now takes the attribute; the
inventory preview divides it out as extractEntityInInventory does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: read in full (pr-triage-1). A keyboard key bound to attack/use keeps clicking while held, as KeyboardHandler.keyPress sends every repeat to KeyMapping.set/click; horizon-ctl 'repeat' keeps the key held. Unit test added.
At startup, when a person is playing (not a server, benchmark or automated run), the game puts
"Horizon Official" (horizon.bunchy7s.dev) first in servers.dat, once: horizon_official_server.txt
beside it remembers that it was offered, so a player who deletes it never gets it back, and a list
that already has its address (any case, with or without :25565) is left as it is.
servers::prepend inserts the entry at the root level, so hidden entries and fields the codec
doesn't know are kept, and leaves an unreadable file alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by Ludovic's AI agent (Claude): the review points are addressed (tagged pigs, enclosed arenas, cleanup); flow-push and bubble-column pass on the server thread and in-frame (current carries 2.8/2.7 blocks vs 0 in still water; the column lifts higher than plain water). Tools only. Queued for the merger. Re-run at 9039a268f (arenas now built at y 100, husk line back to main's): lan_e2e flow-push and bubble-column PASS on the server thread (flowing 2.87 vs still 0.0; column 1.404 vs plain 0.785) and in-frame (2.776 vs 0.0; 1.42 vs 0.725).
A throw on an item tab's display slot (Q or a right-click in the creative
screen) dropped the stack with a local Player.drop. On a server connection
(single player's server thread, LAN, multiplayer) that item entity was
never sent to the server, so it vanished. Send the stack as
SetCreativeModeSlot to slot -1 instead, as the survival tab's throw and
vanilla's handleCreativeModeItemDrop already do.
Vanilla: CreativeModeInventoryScreen.slotClicked (item tab THROW),
MultiPlayerGameMode.handleCreativeModeItemDrop.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
load_chunk put each loaded block entity into the map over any entity
already at its position and kept that position's ticker slot whatever
the new block state was. Vanilla's LevelChunk.setBlockEntity drops the
previous entry, and updateBlockEntityTicker rebinds the ticker only when
the new state has one, removing it otherwise.
Mark replaced positions and, at the next tick, free their slot unless
the block state ticks. Add a test covering a replacement that keeps the
slot and one that frees it.
Vanilla: LevelChunk.setBlockEntity, LevelChunk.updateBlockEntityTicker,
LevelChunk.registerAllBlockEntitiesAfterLevelLoad.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
🤖 Agent: verified by Ludovic's AI agent (Claude) on union/pt2-a 7e322f591 (main + 8 PRs): fmt, workspace clippy -D warnings, horizon-client/protocol/command/client-core/world tests, fast build; lan_e2e hurt, cure-discount, commands, piston-push, hands, helduse on the server thread and in-frame, plus portals (server thread): all pass (cure-discount: 14 wheat for the cured villager's emerald, host and guest). Queued; the merger waits for CI parity on this head.
- Track keyboard_mouse ownership and view_at viewport offset on Game
- Ignore cursor grabs, visibility changes, and pointer warps when not owning keyboard/mouse
- Offset cursor warps by view_at in shared windows
- Track physical_buttons and clear on focus loss
- Add capture unit tests for controller-only games and viewport offsets
Co-Authored-By: Antigravity <[email protected]>
Lowering the render distance (the Performance preset after 32 or 64) let the coarsest far tiles stand in for the chunks it left: the tiles planned there had been inside the full-detail area, so never sampled, and the 64-block cells of the roots showed flat plateaus with stone walls for minutes. Those columns now stay left out, their meshes drawn, until the tile planned over each is ready.
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
Resolve the carried-block overlap with #2986 while preserving upstream's iron
golem flower layer, fox item context, minecart changes and renderer crate split.
Keep the captured carry transform and per-face BlockTintSource defaults; remove
the superseded single-tint helper that only the old enderman path used.
Carry the tint implementation into horizon-client-render/src/mesher.rs and
update the differential harness's source path after the upstream module move.
Resolved by OpenAI Codex on behalf of NetuosoDEV. No additional local tests
were run, as requested by NetuosoDEV; the updated PR head is checked by CI.
Co-Authored-By: OpenAI Codex <[email protected]>
renderArmWithItem draws an empty main hand with renderPlayerArm on the
player's main arm (the left one when the main hand is set to left) and
only when the player isn't invisible. hand_geometry always drew the right
arm and drew it while invisible. The map arms (#3444) already follow
both rules.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
Use the baked block-state model instead of the block item's model so flowers,
fungi and roots keep their cross geometry and state properties survive. Remove
the item-centering compensation while retaining the vanilla carry transform.
Apply BlockTintSource.color(state) to tinted block-model faces using the active
grass colormap and vanilla default foliage colours. Keep in-world sugar cane
and lily pad colours unchanged. Use BlockModelResolver's fixed model seed 42.
Add a differential adapter checking 6,616 state/tint-index combinations against
Minecraft 26.3 and a scratch-world offscreen screenshot driver. The existing
captured-vertex transform regression now checks block coordinates directly.
Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV manually tested
the fix in game and confirmed it works.
Co-Authored-By: OpenAI Codex <[email protected]>
Joining a server whose resource pack is already in the download cache
marks the file used (HttpUtil.updateModificationTime, ported as touch),
and the overlay cache keyed a ZIP pack by its stamp, modified time
included. Every join therefore rebuilt the whole overlay and reloaded
from thousands of new files: on Windows, where Defender scans each, a
join waited about a minute on "Preparing resources" (a 1.2 MB Nexo
pack on tensa.co.ua). A ZIP is now known by its bytes, one file read;
directory packs keep their stamps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An item file may carry `components`, a 26.3 data component patch over its
template's: `minecraft:<type>` sets one, `!minecraft:<type>` with `{}`
removes it. A modded sword gets its own damage and durability, a tool its
mining speed, without a template per stat. Types no vanilla item has are
refused, so a typo fails the load. Issue #3305, item 3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added block_model_vertices_tinted (one tint for every tinted face)
for mob_blocks.rs; it now calls this branch's tinted_block_model_vertices
with that tint for tint indices >= 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A zip entry named `../x` or `/x` (or `C:\x` on Windows) was written as it
was, outside `<world>/datapacks/<name>-26.3/`. `read_pack` now takes each
entry's name from `enclosed_name()` and refuses a pack with one that has
anything but plain components, and `upgrade_into` refuses to write such a
path. Review on #3369.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: PR triage (Ludovic's AI agent, Claude): read in full, building on rdbt's approving review. Enderman's carried block is its baked model (seed 42, not centred) as CarriedBlockLayer, FoxHeldItemLayer draws the mouth item in the ground context (sleeping/baby offsets), IronGolemFlowerLayer the poppy while getOfferFlowerTick() > 0, Skeleton.DATA_STRAY_CONVERSION_ID (16) is sent and read so freezing skeletons shake, and the shadow radii match GiantMobRenderer (0.5 x 6), ElderGuardian 1.2, HappyGhast 2.0, Armadillo 0.4, Shulker 0. verify green; CI parity on this head.
#3085 already moved the bow, trident, throws, potions and wind charge to
known_player_movement(), and main has the Windows test fixes (#3088, and
#3587 for port_publish.py). This branch takes main's version of all of
those. What it keeps: Guest::known_movement, set on every accepted move
and zeroed by a ClientTickEnd without one (handleClientTickEnd), and the
save and restore of Game::known_movement in enter_guest/leave_guest.
The moves.last() copies in UseItem, the spear's jab and the per-tick
using turn go, since enter_guest sets the value for every turn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the server thread a reloaded wolf ticks before the player joins, and
the anger ended because no player counted as no valid target. Vanilla's
updatePersistentAnger ends it only once it finds the player and the player
is in creative or spectator, or the difficulty is peaceful.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Beds checked the level's raw thundering flag, whose timer also runs in
clear weather, so players could sleep through a sunny day. The Overworld's
bed_rule sleeps WHEN_DARK, as vanilla's Level.isDarkOutside: skyDarken >= 4,
from the time of day and the rain and thunder levels.
Expose renderer frames and surface handoff, with a standalone two-view GPU smoke example. Part 1 of the splitscreen series; normal game rendering is unchanged.
Co-Authored-By: Codex <[email protected]>
CreativeModeInventoryScreen builds the destroy slot's tooltip from inventory.binSlot; the screen drew the English literal instead, so it stayed English in every language. Read the key through the same translate helper the tab titles use, keeping the English text as the fallback.
Co-Authored-By: jaszkus's AI agent <[email protected]>
A new extra, Own name tag (ownNameTag, off by default), under a Third
person header on the Extras page. With it on, the player's own model in
third person gets the name tag other players see: the profile name in
its team's colours, the below-name score, faded while crouching. It goes
through the same checks as other players' tags (team name tag
visibility, distance, F1) and hides in first person, while sleeping, in
spectator mode and while invisible. Vanilla never names the camera
entity, so this stays off by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
Every row on that page is a Nightly now, and its title was the install
folder name (nightly-2026-10-29-4baa571) because version_tag only strips
the v of a release tag. nightly::label turns it into "Nightly · 4baa571";
the date stays beside it, where both call sites already print it.
Fix#3700.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Both languages stay in Language::ALL; the PR's loop over every language
replaces main's fixed list in the completeness test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Text displays: main's text_display::draw and Lines (#2812) replace this
branch's inline drawing, with this branch's display_light (the same light);
this branch's display_data and transformation are kept beside them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
chicken_panic_192 fails without the Chicken water malus (nav path of 7
nodes through the pool instead of vanilla's 5).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Conflicts:
- game/stations.rs: keep both tooltip lines, the ominous bottle's before
the suspicious stew's, in ItemStack.addDetailsToTooltip's component order.
- item_tooltip.rs: keep both tests; the stew test now expects main's
zero-padded durations ("00:03", "00:07").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
tools/port_publish.py: main already writes the store path with Posix
separators and ~ (the same fix), so main's version is kept; the roadmap
UTF-8 reads and writes remain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On macOS the temp folder is under /var, a symlink to /private/var. dependencies()
resolves each path before comparing it with ROOT, so an unresolved temp ROOT
dropped every file and the test failed on macOS (Linux CI didn't see it).
The real ROOT is already resolved.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
hud.rs: main's name_tag (and its test) and this branch's draw_inline_object
(and its tests) both kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote.rs: main's spectator check and off-hand anchor charge are kept with
this branch's InteractionResult: an anchor charged from the off hand is
SUCCESS (RespawnAnchorBlock.useItemOn) and swings, a jukebox taking a disc
is SUCCESS (JukeboxPlayable.tryInsertIntoJukebox), an anchor left to the
off hand's glowstone is PASS. level/tests.rs: both new tests kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote.rs: main's spectator check (a spectator's use predicts nothing) and
this branch's shelf prediction are both kept, the spectator first.
machines.rs: the new Brushable block entity made the dispenser harness's
dropper under suspicious sand treat it as an empty container and drop
nothing; BrushableBlockEntity is no Container, so the dropper drops in
front of it as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cod, salmon, tropical fish, pufferfish, tadpoles, squid and dolphins keep
PathType.WATER at 0 (WaterAnimal and AgeableWaterCreature constructors).
Horizon left it at 8, so DefaultRandomPos rejected every water position
and RandomSwimmingGoal retried ten times, ending on a spot in the air.
FishMoveControl also computes its yaw like MoveControl does.
Adds cod, salmon and tropical fish scenarios to the mob_ai harness; the
probe names the fish goal FishSwimGoal as vanilla does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Freeing the hundreds of meshes a chunk border brings made the render thread wait on the allocator behind the busy meshing and generation workers, up to 20 ms of a frame at each border while flying at render distance 32. The renderer now hands them back to Streaming, whose workers free them ahead of other jobs, as vanilla's compile task closes its mesh data on its own thread. Refs #1890.
SpawnEggItem.useOn on a spawner takes the type from getType, the egg's
entity data id when it has one, and for an op (PlayerList.isOp) calls
Spawner.setEntityData: TypedEntityData.loadInto sets the next spawn
data's id and merges the egg's data into it. Horizon set only the egg's
own type, so a custom egg gave a plain mob.
The acting server guest's permission level is kept beside its name and
UUID (lan_acting_permission), so the check is the acting player's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Mods page for a selected Java instance (its version and loader; no Horizon port check). Horizon instances unchanged. Verify green; matrix 40873026396 green on Windows, macOS and Linux.
🤖 Agent: verified by pr-triage-3. Now only the kelp mipmap fix in gfx/animate.rs (the save.rs part is on main via #3088, port_publish.py is in #3587, as the author offered). union/pt3-2 285ecb6e8 (main 2b294bbe3 + #2417#2951#2981#3000#3417#3433#2544#2586): fmt, clippy -D warnings (horizon-client, -client-render, -client-core, -world, -items, all targets), their tests pass.
🤖 Agent: Java instances use their loader for mods; copy-from keeps a Fabric or Quilt loader. Verify green; matrix 40858947625 green on Windows, macOS and Linux.
🤖 Agent: new accounts' token from launcher/login, falling back to login_with_xbox; the owner gate is unchanged. martipops confirmed it with a new account on an online-mode server. Verify green; matrix 40858081820 green on Windows, macOS and Linux.
* The dragon follows the server that flies it
The client draws the dragon from its own state (game/dragon.rs), but a
server's dragon only sent its health, its phase and its damage: its
moves never reached that state, so the dragon was drawn where it had
spawned while the server's dragon flew around the island, and the local
flight machine flew it away from the server's moves as well.
The server's move packets now move the dragon the client draws, the
way every other entity of a server's follows them, and a server's
dragon is no longer flown by the client's own phase machine: the
server flies it, the client flaps its wings, keeps its flight history
and places its parts.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
* Keep the steering comment
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
* Ease the server's dragon through the lerp tick_remote_dragon reads
The frozen dragon came from move_entity: the dragon also has a copy in the
mob list, so its moves took the mob path and returned before r.lerps, and
tick_remote_dragon never had a target. The moves now skip that copy and
reach r.lerps, so tick_remote_dragon eases the drawn dragon over the three
steps of LivingEntity's interpolation, as the code it already had meant to.
This replaces the snap to each move and the remote flag in tick_dragon:
a server's client never runs tick_dragon (tick_end is the local level's),
and the snap left the dragon still between the server's updates.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Code <[email protected]>
Co-authored-by: minimax-m3.1-flash (Claude Code) <[email protected]>
Co-authored-by: Ludovic Marie <[email protected]>
Both fluid checks built their walls at the host's feet, in the host's chunk,
so a player standing there could be shut in stone and suffocate. They now
build at y 100, as check_piston_push does, and the last clear takes the
floor away as well. The boat dismount check's husks go back to NoAI, as on
main: that change belongs to no fluid check.
flow-push, bubble-column, digging and frames pass in that order on the
server thread and --in-frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first commit replaced the tick_items span with the gateway handler, so
dropped items stopped falling, merging and despawning in every mode. The
call is back, next to the gateway's.
An entity that has just come through a portal carries PortalCooldown 300.
EndGatewayBlock.entityInside still triggers the gateway's own cooldown, but
Entity.setAsInsidePortal then only restarts the entity's cooldown, whatever
the portal: the gateway now asks the shared portal state first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve HUD text rendering conflict by preserving inline player and atlas objects alongside the new world text shadow and depth behavior.
Co-Authored-By: Codex <[email protected]>
/place template listed worldgen/structure/minecraft only, so a content
mod's templates (worldgen/structure/<ns>/...) were never suggested.
Also drop the unused templates additions from the content merge.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). sounds.json files are listed once per namespace instead of a lookup per sound, same resolution; its earlier parity failure was the remote_fireball wind-charge load flake, and every other selected harness passed. union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
🤖 Agent: ci-queue-1: world_spawn run by hand on 3 seeds (3/3 match with the cache empty, then reused: 12 s -> 4.7 s a seed; PARITY_JAVA_CACHE=0 reruns vanilla); new unit tests; pr_ready --quick green (411 Python tests).
🤖 Agent: gate cg29: the parity failure at e9c6751a1 was command_exec's vanilla oracle (it threw 'entities around the player did not load' on playhorizon), not this PR; the other 47 selected harnesses passed in that run, and command_exec passes on main + this head on the Mac mini. Head unchanged since cg27's proof.
🤖 Agent: gate cg29: the parity failure at cf0b111bd was remote_fireball's wind charge (3 of 6 samples), a load flake of that harness seen on unrelated PRs today; this PR only touches chat.rs. union/cg29-1 (main b19b4e92d + #3104#2799#2819#2812): fmt, clippy -p horizon-client/-client-render/-client-core --all-targets -D warnings, tests 1334/0, fast build; lan_e2e team-push pvp frames minecarts boats deflect cushions commands tab on the server thread and --in-frame all pass. CI parity rerun on this head.
🤖 Agent: proven by contrib-gate-28 (requeued after a cancelled parity run): gate fix for a trailing § in a part plus a test, main merged after B9; fmt, clippy -D warnings, tests pass; text_display_render 600/600 cases match vanilla; CI parity for the rest.
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
The content merge wrote a mod's worldgen into the worldgen bundles but
never into command-registries.json, so /locate structure and /place
structure/jigsaw/template suggested vanilla ids only, though the commands
themselves accepted the mod's.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
- run-parity.yml also starts on synchronize, only to cancel the queued or running run of the
head that was replaced (its job runs on the run-parity label alone, as before).
- Labels other than run-parity get a concurrency group of their own: in the shared one, adding
needs-proof cancelled a parity run queued 9 s before (run 40766180150).
- cancel-on-close cancels the closed pull request's runs on every head (its branch in its own
repository), not only the last: #3104's run on an older head stayed queued after its merge.
Co-authored-by: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. One small follow-up for later, not blocking: Player.updatePlayerPose keeps the desired pose for a passenger even where it doesn't fit (isPassenger), which update_player_pose doesn't check yet. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
🤖 Agent: read in full (pr-triage-1), checked against vanilla 26.3 StringUtil.formatTickDuration (Mth.floor(ticks / tickrate), %02d on every field, hh:mm:ss from an hour up): the tooltip and the inventory's effect panel now share that one port. Client text only.
Windows reports AltGr as Ctrl plus Alt, and a layout's third-level
characters (ABNT2 `/` on Ctrl+Alt+Q, German `@ { [`, Polish letters)
arrive as text while a Control key is down. The chat box, the command
block's command, the world options' fields and the recipe and mod-panel
search boxes dropped any typed text while Control was held, so none of
these characters could be typed.
Vanilla's KeyboardHandler.textInput hands every code point to charTyped
whatever modifiers are held; EditBox.charTyped only checks
StringUtil.isAllowedChatCharacter. Text now reaches those fields unless
the edit modifier is held without Alt (where AltGr is Ctrl+Alt), so
Ctrl and Cmd shortcuts still type nothing.
Fixes#3001
Co-Authored-By: Claude Opus 5.5 <[email protected]>
open_mod_screen returned early while the title was up, so the Settings
button at the title did nothing, and the mod screen was neither drawn nor
given clicks or keys there. Draw it like Options (before the title), route
its clicks and keys, and blur the panorama behind it. A mod's own screen
(ui.open-screen) still waits for a world.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Review fix on #3498: the 5-arg ItemEntity(level, x, y, z, stack)
constructor draws its push from this.random, a fresh source — not
this.level().random. Death loot, gift loot and MobEvent::DropItem now
use dispenser::new_item_motion() (already the item's own random), and
the DropGift arm on main had the same level-random draw and joins them.
The level random stays untouched for whatever reads it next (orb merge
ids, block ticks).
Co-Authored-By: JustKarkat's AI agent (ZCode)
Vanilla's server is ~11 s of each seed's ~13 s, 60 seeds a run, and world_spawn
was 25 % of the PR parity runners' harness time on the Mac mini (37 % on
playhorizon): parity_shim.py never caches it, since a server's console is a pipe.
Its spawn depends only on the pinned jar, the JDK, the command line and the
seed's server.properties, which make the key. --fresh, --all and parity.py's
fresh retry of a failure create every vanilla world as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OpenGL swapchain images offer RENDER_ATTACHMENT only, so the surface is
configured without COPY_SRC, but the screenshot path copied the swapchain
image into the readback buffer anyway: wgpu's validation aborted the game
("Usage flags RENDER_ATTACHMENT ... do not contain required usage flags
COPY_SRC") on every screenshot under the GL backend.
A frame that is read back on such a surface is now drawn into a texture
that can be copied from, read back from there, and then drawn onto the
swapchain image (wgpu::util::TextureBlitter), so the window still shows
it. Other frames, and surfaces with COPY_SRC, draw straight to the
swapchain as before. The check in write_screenshot that refused every
readback on these surfaces goes with it.
Fixes#3510
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The wheel over a bundle only reached bundle_scroll on the survival
inventory and container screens. On the creative screen it always
scrolled the item grid, and the hover tick that clears the selection
(onStopHovering) didn't run there either.
CreativeModeInventoryScreen.mouseScrolled calls
AbstractContainerScreen.mouseScrolled first, which offers the wheel to
the hovered slot's ItemSlotMouseActions (BundleMouseActions), and only
scrolls the grid when none took it. takes_bundle_wheel names the
screens that do this (inventory and container menus, and creative), and
both the wheel dispatch and tick_bundle_hover use it.
Fixes#3211
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bolt quads (RenderTypes.lightning has no texture) sampled the atlas at
the white sprite's corner (u0, v0). That point is on the sprite's border,
where the atlas's nearest sampler picks the neighbouring transparent texels
for most fragments, so a bolt came out as a few scattered pixels in
singleplayer and on servers alike. Sample the middle of the sprite, as the
clouds do.
Fixes#3171
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The check looked up `minecraft:mining_fatigue`, but the player's effects are kept by their bare name, so it never matched: the curse, its sound and the apparition came back at every 1200-tick pulse. As MobEffectUtil.addEffectToPlayersAround does, a curse as strong that doesn't end within the next minute now stays.
Address the review on #3544:
- goals::Cx carries the level random (the one the brains already get);
the weighted jump-candidate pick and the three TIME_BETWEEN_LONG_JUMPS
samples draw from it, as LongJumpToRandomPos/LongJumpMidJump do.
- The angle shuffle in calculateOptimalJumpVector uses a fresh unseeded
random (Collections.shuffle's global one), not the frog's.
- The preferred-block roll stays on the frog's random (vanilla uses the
body's random there).
- TODO in LongJump::can_use for FrogAi.initJumpActivity's TEMPTING_PLAYER /
BREED_TARGET gate and LAY_SPAWN priority: frogs have no tempting,
breeding or frogspawn here yet.
Other mobs only get the new field threaded through; nothing else reads it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Photon v1.4's gbuffers programs make naga's SPIR-V writer panic
("Expression [89] is not cached!") while wgpu-hal's Vulkan backend makes
the render pipeline in PackPost::new. Nothing caught it, so the game
crashed. The pack stays picked in horizon-graphics.txt, so every later
world load crashed again until the file was edited by hand (#3525).
- crash::contain runs a closure under catch_unwind and returns its
panic as an error. The panic hook logs such a panic with its stack
and makes no crash report.
- pack_post::checked, which builds every pack program's pipeline under
a validation error scope, contains a panic too. The pass is skipped
with a note, as a validation error already skips it. The rest of the
pack runs, and the device keeps working.
- Any other panic while loading a pack (translation, PackPost::new)
turns the pack off. Nothing runs in its place, it is unpicked and
saved so the next start does not load it again, and the chat says so
in red, as a refused Horizon pack already does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WitherBoss.hurtServer rejects damage caused by a non-player mob in
#wither_friends (the undead, withers included). Horizon only filtered
the wither's own targeting, so skulls and explosions from one wither hurt
its neighbours, which then retaliated through HurtByTarget.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
On the Simple and Advanced graphics pages, the scrollbar was drawn but
GraphicsUi::click never checked for it. A press on it did nothing, and
a drag only moved a held slider, so the list could only be scrolled
with the wheel (#3552).
GraphicsUi now keeps its scroll in ui::Scroll, the AbstractScrollArea
port the other lists use:
- a press on the scrollbar column holds it, silently (updateScrolling);
- dragging moves the list with the pointer, and going above or below
the list jumps to an end (mouseDragged);
- releasing lets go (onRelease).
The column is computed once, for both drawing and input.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff, no conflict). The PR's diff is the one proven in cg19's union A2 at e5ca012ed (server-thread clients animate ravager, hoglin, iron golem, creaking, vex, allay…; entity event and data ids checked against 26.3 then). union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
The loader's result was taken by App::about_to_wait to end loading, but the
event loop polls once more after exit(): the loader had finished, its channel
was closed, and poll_menu_resources replaced the real error with "resource
loader stopped before finishing". Every failed load (missing or stale prepared
files) ended with that message.
- Loading keeps its first error and stops polling once Done has arrived.
- A loader that really panicked reports its panic message.
- Before any window or loader, a missing horizon-resources.json names the
folder it was looked for in and how to prepare it (the launcher, or
horizon-prepare --prepare).
Fixes#3597
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: read in full (pr-triage-1). Item names use Util.makeDescriptionId's key in the item's own namespace (item.<ns>.<path with / as .>, then block.), so mod items translate; vanilla items unchanged. Approved by rdbtCVS.
sky.rs draws the night look's backdrop: the sky in texel bands, stars that
twinkle in steps, three layers of hand-drawn clouds drifting a texel at a
time, a sun cut in slices and two skylines. Sprites are baked once per
appearance and scale into tiles that don't depend on the window's size; a
frame is asked for only when something on screen changes, never under
reduced motion (macOS, or HORIZON_LAUNCHER_MOTION=off), and not 10 s after
the window loses focus. It replaces the stone texture and Home's landscape.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Java's ZipFile.getEntry falls back to the folder entry `name/` when no file has the name, and vanilla reads its bytes like a file's; Horizon left folder entries out, so Loka's server pack had no metadata and the reload failed. Fixes#3413.
🤖 Agent: gate cg29: parity failure at 5f80ddffc was the remote_fireball wind-charge load flake. Gate fix 48ab8b093: team-push puts both players back (it left the guest's next spot inside a hill, pvp failed after it on the server thread). Team by profile name matches Player.getScoreboardName / createEntityFromPacket. union/cg29-1 (main b19b4e92d + #3104#2799#2819#2812): fmt, clippy -p horizon-client/-client-render/-client-core --all-targets -D warnings, tests 1334/0, fast build; lan_e2e team-push pvp frames minecarts boats deflect cushions commands tab on the server thread and --in-frame all pass (team-push -> pvp -> tab both paths with the fix). CI parity on this head.
The model check looked up `minecraft:invisibility`, but the player's effects are kept without the namespace, so the body always showed. As LivingEntityRenderer.submit does, the body is now skipped unless the player's team sees friendly invisibles, while armour, elytra and held items still draw. Fixes#3615.
🤖 Agent: the nightly's untrusted macOS jobs move to ludovics-mac-mini-nightly (registered, online, same trust as horizon-game, never the release runner). test_nightly updated; verify green.
Send the stored custom-name component through the server inventory codec and use vanilla float-to-channel conversion for RGBA shadow colors. Cover both paths with focused tests.
Co-Authored-By: Codex <[email protected]>
On a server, and in single player on its server thread, the window's Game
is a client: `drop` called drop_held there, which spawned a client-only
item entity and left the server's inventory untouched (the server still
counted the dropped items). It now goes through remote_drop, as the Q key
does: predict the removal and send PlayerAction DROP_ITEM / DROP_ALL_ITEMS.
The agent-control e2e drops a plank and asks the server how many are left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Fabric and Quilt for Java instances: profiles from their meta services, libraries sha1-checked, paths through mojang::relative; inheritsFrom loop guard with its test. Verify green; matrix 40855348257 green on Windows, macOS and Linux.
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). header_separator/footer_separator are 32x2 textures: tiling them as 32x32 squares sampled 2/32 of the height. tiled_sized tiles them at [32, 2] (both rows, last column clipped), with a unit test; ui::tiled keeps its square form. union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
🤖 Agent: read in full (pr-triage-1). A dev-only Python tool (tools/hdr_gallery.py) that writes a datapack of every light-giving block into a world folder for tuning HDR; no network, no subprocess, reads only the prepared facts files, writes only under the given world's datapacks/. Nothing players run changes (skip-release).
The untrusted (nightly) build ran its macOS jobs on the horizon-game runner,
which also takes every pull request's parity job. On 2026-10-29 the nightly's
macOS build sat queued from 08:17Z behind 100 run-parity runs (oldest
00:24Z), so no nightly shipped for hours.
It now runs on ludovics-mac-mini-nightly (labels self-hosted, macOS,
horizon-nightly): the same machine and trust as the horizon-game runner,
still never the release runner. The runner is registered and online.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Draws the night tokens' type and colours in the launcher itself, over the
sky's stops, with each text colour's contrast on a plate over the brightest
stop and the rounded and pixel-cut plate corners side by side. Not a page
players reach; for checking the tokens in the real renderer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_theme::night: the night sky look's colours (NIGHT, and DAWN for Light),
type steps, radii and the Bricolage Grotesque cuts (tools/bake_bricolage.py,
OFL), with AA tests over every sky stop. tokens.css gains --night-*. The
launcher reads them: Night::blocks() dresses the screens not rebuilt yet in
the night's colours, and Bricolage replaces Geist and Pixelify Sans there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
crates/horizon-client/assets/horizon/lang/uk_ua.json translates the
game's 13 own strings (the controller options), bundled like ru_ru;
vanilla's uk_ua covers the rest of the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher/lang/uk.json translates the launcher's 194 strings, Horizon's
own, written from the English; Language::Ukrainian ("uk", Українська)
joins the picker, the system language pick and `settings language`. The
translation test now covers every language but English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: imports from Prism/MultiMC/official launcher, read-only on their folders (copy into a new Java instance), formats from public docs. Verify green; matrix 40853481205 green on Windows, macOS and Linux.
🤖 Agent: Modrinth for Java instances, filtered by version and loader; downloads go through download::save (safe names, checksums). Verify green; matrix 40853843158 green on Windows, macOS and Linux.
Fixes missing item break sound effect and broken statistic tracking:
- When a tool breaks from mining a block (mine_block_with_held), route
durability wear through damage_held rather than clearing the slot silently.
- Route weapon wear in mace.rs and brush wear on armadillos in creatures.rs
through damage_held.
- Play entity.item.break sound and re-apply equipment when worn armor
breaks from Thorns in enchant_effects.rs.
Ukrainian headings, buttons and navigation need the capital І (U+0406),
which upstream doesn't map, so it came from the fallback font, smooth
and taller than the pixel letters around it. As #2844 did for О, it
takes each weight's own Latin I glyph, the same shape. Only cmap changes
(and head's checksum adjustment); every other table, the timestamps
included, is byte-identical to the files it replaces.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: requeued by pr-triage-3 after a main merge (was dc3fc6961, cg27-verified, rdbt approved; stopped on a conflict with #2894). Both LOD tests kept. union/pt3-2 285ecb6e8 (main 2b294bbe3 + #2417#2951#2981#3000#3417#3433#2544#2586): fmt, clippy -D warnings (horizon-client, -client-render, -client-core, -world, -items, all targets), their tests pass.
A Java instance can play with a mod loader: `instance create
--minecraft 1.21.4 --loader fabric|quilt [--loader-version V]`, or
`instance loader <instance> fabric|quilt|none [--version V]`, with
`--list` for the loader's versions (the newest stable one by default;
Quilt's list comes unordered, so versions are sorted).
Play installs the loader's profile once from its metadata service into
versions/, beside the game's, with its libraries in libraries/, each
checked against its sha1 (the profile's, else the one its Maven
repository publishes), then starts the profile merged into the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`complete_assets` also runs when Play is pressed, so a store with most of its objects
missing made the player wait for thousands of downloads and a sign-in with nothing on
screen. `refill_own_store` now runs in `reuse_prepared_everywhere`, on the start-up
thread, and Play only copies what is there; the gate's test pins both places.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Since #3596 installing a native mod turns native mods on after a notice;
the intro and the Trust section still said they were off by default. And
a full game build on Linux is proven (an x86_64 server with no Rust of its
own, the launcher's toolchain, Zoom's proof passing on the built game), so
it leaves "Left to do" for the measurements.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Play for Java instances. Diff reviewed: the owner gate holds (a session only for an account that owns the game; downloads via download_version_for_owner); #3451's guards kept. Verify green; build matrix 40851406886 green on Windows, macOS and Linux.
ObjectContents.visit uses the object's defaultFallback without one:
[item/porkchop], [porkchop@gui] off the blocks atlas, [Steve head].
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.startDestroyBlock refuses before predicting when
Player.blockActionRestricted holds: an adventure player without a
can_break item. The client predicted the break anyway (sound, crack,
the block gone) and the server sent it back, and continueAttack swung
every tick. A stack the server sent with can_break still breaks; the
server judges its predicate.
Fixes#3027.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
Ports CustomHeadLayer.submit for players and humanoid mobs:
- Renders carved pumpkins, jack o'lanterns, and mob/player heads worn
in the head equipment slot on the player model and humanoid mobs.
- Adds head_item_geometry to mob_block_geometry, player_model_geometry
(third person), and draw_player_preview (inventory preview).
- Correctly skips armor helmets (handled by HumanoidArmorLayer) and
body-slot species (equines, wolves, etc.).
- Keeps player model extra.head and extra.armor synchronized in
tick_player_model.
- Updates tools/port_map/client-005.tsv mapping.
The first fix covered same-length rewrites through Fixture::write only;
directory_inputs_config_and_owned_output_recovery still failed about
one run in seven under WSL2, through its own fs::write and set_times
and through files added to a directory in the build's tick. Waiting for
the filesystem's clock to move after each rebuild_matches covers every
edit that follows, as a person's next edit comes after the build. The
wait writes new bytes each time (NTFS keeps the time of a write that
changes nothing) and gives up after a second.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's decision 10-29 (native mods on when one is installed, notice once). Diff reviewed; verify green; build matrix 40848959744 green on Windows, macOS and Linux.
TranslatableContents.decomposeTemplate is already ported as
text::format_translation (%s, %n$s, %%), so the modifier line uses it instead
of a second, partial parser.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game.rs/remote.rs: main's creative screen reopen (#3187) and code of conduct
reset are kept beside this branch's tutorial steps. tests/test_remote_mining.py
compiles remote.rs's mining against its own small Game, which now needs a
no-op tutorial_destroy (verify failed with E0599 there).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The same Windows path fix is in #3587 (with the UTF-8 body file), so this
branch keeps only the kelp mipmap change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lod.rs: main's chorus plant test (#2894) and this branch's small island test are
both kept; docs/graphics.md names chorus plants and small islands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
save.rs: main already reads session.lock after the lock is dropped (#3088), so
main's version of that test is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
net.rs: main's configuration loop now hands the code of conduct to the game
(Event::CodeOfConduct) instead of accepting it; this branch's UpdateTags arm is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by contrib-gate-27. Read in full; matches StructurePiece's isSolidRender checks and the reorient order (N, E, S, W). union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
#damages_helmet holds falling_anvil, falling_block and falling_stalactite (26.3's
tag): main added the stalactite, this branch the anvil; both kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A version file with `inheritsFrom` (what Fabric, Quilt and other mod
loaders install) is merged into the version it names, as the official
launcher reads it: its fields over the parent's, its libraries first (a
parent's library it names again is left out), its arguments after the
parent's, and the parent's client jar (`jar`). On a version from before
1.13, a profile's own argument lists join the old line instead of
replacing it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
options.audioDevice and options.audioDevice.default come from vanilla's
language files ("Device", "System Default"): the game's own lang no longer
overrides them. Audio keeps the device asked for, so a saved device that is
unplugged plays on the default without reopening the stream at every
settings change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
roadmap.py reads and writes ROADMAP.md and the roadmap TOML as UTF-8, so
build writes the · as git has it and build --check doesn't call a fresh
checkout out of date on Windows. port_publish.py decodes gh's and
node's output as UTF-8, so an issue body's arrows survive an update, and
writes the store path as #3441 does, so the two pull requests agree.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's c5fe5d4bc added EndState::eye_positions (id and position, for
status); the hitbox view needs the previous and current positions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Unix the overlay cache compares stamps (length, mtime, ctime, inode),
and Linux takes them from a coarse clock, so the fixture's same-length
rewrites within one tick looked unchanged:
normalized_and_external_overlay_roots_keep_merged_bytes_current failed
about three runs in five on upstream main under WSL2. Fixture::write now
waits until the file's stamp moves when the bytes change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps Spanish beside German, adds the six strings main added since, and drops
the nightly strings main removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher), launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40817273793), pr_ready --quick green with the new test deleted_instances_go_to_the_trash_and_are_forgotten; docs/launcher-commands.md regenerated.
Ludovic's decision (2026-10-29). Native mods were on only once the
player found Settings > Native mods (experimental). Now installing a
native mod turns them on, so Settings offers its Build at once; players
who never install one keep them off.
The first native mod's Install shows a notice once, before anything is
installed: native mods compile into a game of their own, the first
build downloads Rust and can take 20 to 30 minutes, later builds are
faster (Install and turn on native mods · Cancel). `mods install`
turns them on the same way and prints the notice. The build itself
stays behind its trust warning, now saying 20 to 30 minutes too, and
turning a mod off in the game's Options > Mods is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameRenderer.shouldRenderBlockOutline() checks the hidden HUD (F1) and the game mode, not the open screen, and Minecraft.tick() keeps the block raycast current while chat is up. Horizon also required Screen::Playing, so the outline vanished as soon as chat opened.
Co-Authored-By: jaszkus's AI agent <[email protected]>
Roadmap (from the 59 suggestions in the queue and what merged since):
- Microsoft sign-in shipped in [email protected].
- New: Minecraft Java instances in the launcher, native mods built by the
launcher, the port ledger, a new launcher design, hosting panel setups
(Pterodactyl, AMP), joining older-version servers through a native
protocol adapter, Windows ARM64, splitscreen (#2054).
- Mods items reworded for native hook points instead of the WebAssembly
API; scripts for mods moved to Later; asked-for client mods added.
README: more than 300 harnesses, the Nightly channel, native mods, and
the port ledger as a second, method-by-method measure.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
26.3 asks for a cursor from every hovered widget (AbstractWidget
handleCursor: the pointing hand, or not-allowed when inactive) and from
the creative inventory's unselected tab buttons
(CreativeModeInventoryScreen.extractTabButton: the pointing hand while
the whole-pixel mouse is strictly inside the 26x32 button). #1347 did
this for ui::button and the pack screen, but the title screen draws its
own buttons and the creative tabs never asked, so both kept the arrow.
The title screen's button loop now asks through widgets::widget_cursor
with the mouse test apart from the enabled check, and draw_creative asks
for the hand through the new Creative::tab_wants_hand.
Fixes#3040.
Fixes#3042.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
PhantomRenderer.scale was not ported: the phantom's model is laid out about
1.3 blocks above the feet and vanilla moves it down (and 0.1875 along z)
after the size scale, so Horizon drew it above its 0.9x0.5 box. Port the
scale (1 + 0.15 * size) and the translate, and PhantomRenderer.setupRotations'
pitch.
Fixes#3008.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Wolf.aiStep ends with NeutralMob.updatePersistentAnger(level, true): a
wolf with a target is angry at it, and the 20-39 s timer
(PERSISTENT_ANGER_TIME) starts over every tick it holds one. Horizon only
set the anger in hurtServer when the player hit the wolf, so the pack
mates HurtByTargetGoal alerted attacked without the angry texture, and the
random draws of an angry wolf came in the wrong order. isAngryAt(player)
now reads the anger's target, and stopBeingAngry ends it when the time
runs out, the target mob dies, or the player is in creative, spectator
or peaceful. Saving a wolf keeps that anger target instead of replacing
it with the current target.
The mob_ai harness prints a wolf's anger and gets a wolf_hurt scenario.
Fixes#3185
Co-Authored-By: Claude Opus 5.5 <[email protected]>
port_publish names the version store with forward slashes, so an issue
reads the same whoever publishes it (test_port_publish expected
~/code/... and Windows wrote ~\code\...), and writes the issue body as
UTF-8 for gh whatever the code page. test_roadmap reads ROADMAP.md as
UTF-8. All 408 Python tests now pass on Windows, with or without
PYTHONUTF8.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
A painting item with a `painting/variant` component (every one but the
first in the creative tab) placed a random painting and showed only
"Painting". HangingEntityItem.useOn runs Painting.create's random pick,
then EntityType.createDefaultStackConfig applies the stack's components
(Painting.applyImplicitComponents sets the variant) and the painting must
still survive. HangingEntityItem.appendHoverText adds the variant's title,
author and size, or "Random variant" in a creative tab without one.
The component also crosses the wire now (a painting_variant holder,
component 109): a creative client's pick reaches the server, and the
server's stacks keep their variant on the client.
Fixes#3503
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Command blocks barely worked from the default single player (the window
is a client of its server thread), a LAN guest or a dedicated server:
- ServerPlayer.openCommandBlock: the server now sends the block's data
(saveCustomOnly) to the player whose use opened it, and the open
screen refills from it (handleBlockEntityData, updateGui). The chunk's
update tag leaves the command out, so the screen showed an empty box
and Done then wiped the block's command.
- Screen.extractBackground of an isInGameUi screen: the dimmed
gradient, not the in-world menu background.
- CommandSuggestions in the command box with the arguments
AbstractCommandBlockEditScreen passes (commands without a slash,
completions from the start, 7 lines under the box, fill 0x80000000):
the chat's suggestions take a SuggestLayout. Server completions
(ask_server) reach the box too.
- horizon-ctl key presses reach the command block screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientSuggestionProvider.getRelevantCoordinates and getAbsoluteCoordinates
read Minecraft.hitResult only when it is a block hit. Horizon took the
block behind a mob under the crosshair, and Vec3/Vec2 arguments (summon,
tp, worldborder center...) only ever offered ~. Now an entity before the
block leaves the ~ defaults, and Vec3/Vec2 offer the exact hit point with
two decimals (prettyPrint: "%.2f", Locale.ROOT).
Fixes#3427
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractHorse.hurtServer had no port: after a hurt that went through,
vanilla draws random.nextInt(3) and on 0 calls standIfPossible, which
rears the horse for 20 ticks unless canPerformRearing is false (llamas,
camels). Mob::damage_with now ends with that roll for every equine, and
make_mad shares the new stand_if_possible.
The stand counter runs down in AbstractHorse.tick, with or without AI,
but Horizon ran it down only in the AI step, so a NoAI horse that reared
from a hit stayed reared for good. A mob with no AI now runs it down too.
The mob_ai harness gets a llama_hurt scenario (the draw without the
rear), which matched for 10 ticks before and all 201 after; the probe
names MountPanic and LlamaHurtBy as vanilla does.
Fixes#3432
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Key events carry the scancode's US-layout code, which ignores options like
caps:swapescape. Take Escape and Caps Lock from the resolved keysym instead.
Fixes#3133
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Singleplayer and multiplayer run the player through the remote client
path, which dropped the motion's Splash and SplashParticle events. In
vanilla the local player's water-entry splash (Entity.doWaterSplashEffect)
is heard only on its own client: Player.playSound passes the player as
the exception, so the server's copy skips it, and the particles are the
client level's own. Play them from the remote tick as the local tick does.
Fixes#3028.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
IntegratedServer.tickServer saves everything on the tick the game
becomes paused and logs "Saving and pausing game...". Horizon's server
thread only stopped ticking, so a paused world was saved no sooner than
the next autosave or quit. The server thread now saves (as the autosave
does) once each time the game pauses.
agent_control_e2e gets pause-save: opening the pause menu writes
level.dat.
Fixes#3280
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keys held when the player left a server stayed in Game::keys: once the
title or disconnect screen is open their releases go to that screen, so
W and Space held through a LAN host closing the world walked and jumped
on in the next world. Vanilla's Gui.setScreen calls KeyMapping.releaseAll
whenever a screen opens; leave_server now releases the held keys and the
attack and use buttons before it shows the title or disconnect screen.
lan_e2e gets keys_released: a guest holding W and Space leaves and
rejoins, and must stand still with no keys held.
Fixes#3246
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NoAI only skips Mob.serverAiStep; Mob.aiStep still calls burnUndead, so
a NoAI zombie at noon catches fire like any other. Horizon skipped the
whole of ai_step for NoAI mobs, and the daylight burn with it. The burn
is now its own burn_undead, run with or without AI.
The mob_ai harness gets zombie_noai_day_burn (a NoAI zombie in the sun).
Fixes#3430
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OminousBottleAmplifier.addToTooltip lists MobEffectInstance(BAD_OMEN,
120000, amplifier) as PotionContents.addPotionTooltip does: blue (NEUTRAL),
"Bad Omen IV (1:40:00)". Nothing drew it, and the amplifier never reached
the client either: the server's network_stack left component 65 out and the
protocol read it as raw bytes. It now goes over the wire as a VarInt
(OminousBottleAmplifier.STREAM_CODEC) and item_sync decodes it.
Fixes#3501
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server client (single player included) remote_use_item predicted
SUCCESS for every bucket, glass bottle and boat, which ended
Minecraft.startUseItem before the off hand's turn: a Riptide trident
there never charged. BucketItem, BottleItem and BoatItem.use PASS or
FAIL with nothing to fill, empty or place, and startUseItem stops only
on SUCCESS. The client now checks the target first (a waterlogged block
counts as a water source, as SimpleWaterloggedBlock is a BucketPickup);
place_boat is split into boat_to_place (no side effects) so both share
BoatItem's checks. A powder snow bucket (SolidBucketItem, only useOn)
always passes its use.
Fixes#3147
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A wither's block breaking (WitherBoss.customServerAiStep) calls
Level.destroyBlock, which sets the cell to fluidState.createLegacyBlock()
with flag 3. Horizon's mob BreakBlock event wrote air instead, without
neighbour updates: a wither in water carved an air pocket, a waterlogged
block lost its water, and the pool did not flow back in. The event now
sets the fluid's block through setBlock (skipped when that is already
the cell), and the wither's break sound (level event 1022) only counts
cells that changed, as destroyBlock's return value does.
Checked by a new "wither breaks" group in the command execution harness.
Fixes#3153
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreativeModeInventoryScreen.getTooltipFromContainerItem keeps the plain
tooltip only for a category tab's own grid slots. Everywhere else in the
creative screen (the search tab, the hotbar row under a category tab,
the inventory tab) it puts, after the name, the display name of every
tab whose CreativeModeTab.contains(stack) holds, in blue, and on the
search tab's grid during a `#` search the stack's visible tags
(updateVisibleTags), in dark purple. Horizon drew the plain tooltip
everywhere.
Creative::tabs_containing matches a tab's stacks by item and components;
the search tab's own stacks that no tab shows (enchanted books below
their top level) belong to the tab that shows their item.
Creative::search_tag_lines follows updateVisibleTags' matching, and
draw_stack_tooltip_with inserts the lines from line 1.
Fixes#3426
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While a wither charges (Invul > 0), WitherBoss.customServerAiStep returns
before setAlternativeTarget(0, ...), so aiStep has no target to fly to.
Horizon stored the target's position first, so the charging wither flew
toward whatever it targeted. The position is now taken after the charge.
The blast that ends the charge is level.explode(this, ...): the wither is
the source and ServerExplosion's getEntities(source, box) leaves it out.
The wither now names itself as the explosion source (as a creeper does),
and the blast's mob loop skips a source named that way, so it is neither
hurt nor pushed. A source only guessed from the mobs near the centre (a
sulfur cube, already off the list) may be a bystander and is still hurt.
The command_exec corpus gets a "wither spawn" group: a wither summoned
with Invul:20 next to a pig, its position mid-charge and its health after
the blast.
Fixes#3322
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderEyeItem.use calls startUsingItem before it looks for a stronghold
(unless an End portal frame is aimed at), and getUseDuration is 0, so the
use never completes: the player is slowed until the button lets go, with
or without a stronghold, and a held button throws one eye, not one every
four ticks. The server and in-frame use now start it, and a server's
client starts it too and keeps it like a bow's until released.
Fixes#3505
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CopperGolemRenderer's LivingEntityEmissiveLayer (RenderTypes.eyes) draws
the eye texture on a CopperGolemModel baked from ModelLayers.COPPER_GOLEM,
the body's layer. Horizon drew it on copper_golem#eyes, whose prepared model
has no polygons, so the eyes showed the body texture's black sockets.
Fixes#2862
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (and the default server-thread single player) the client runs
the block's useItemOn on a copy of its level to decide whether the click
ends there. horizon-world's use_item_on had no RespawnAnchorBlock case, so
glowstone on an anchor fell through to useWithoutItem (PASS on an empty
anchor) and the client predicted a glowstone block that the server then
took back.
- horizon-world use_item_on: glowstone on an anchor below 4 charges charges
it (RespawnAnchorBlock.charge) and returns SUCCESS.
- The client's off-hand turn runs useItemOn too, so off-hand glowstone
charges the anchor instead of predicting a placement.
- The main hand PASSes when the off hand's glowstone is to charge the
anchor, so a charged anchor isn't used (useWithoutItem) and the off-hand
turn is sent.
Fixes#3034
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: gate cg29: re-checked after your second main merge (70b51333d): same-ray refactor, no behaviour change. fmt, clippy -D warnings, horizon-client tests 796/0 and the 33 harnesses its files reach (33/33) on this head; union/cg29-1 (with #2799#2819#2812): parity --changed 124/124 and lan_e2e frames minecarts boats deflect cushions pvp commands on the server thread and --in-frame.
KeyBinds::translate_all turns a pressed key into the default keys of the
mappings bound to it, so the game's code follows the player's bindings.
It also followed the debug mappings (F3+A reload chunks, F3+S, F3+1 the
profiler chart...), whose defaults are the gameplay keys A, S, D, T, L
and 1 to 4. So after moving key.hotbar.1 off 1, or key.left off A, the
old key was still "bound" to a debug mapping whose default was itself,
and kept selecting slot 1 or strafing left.
The debug keys act only with F3 held (KeyboardHandler.handleDebugKeys),
and debug_keys.rs already reads the physical key and the binding for
them, so translation now leaves them out.
Fixes#3158
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the server thread every player is a guest of the server's Game, but a
villager's hurt gossip (minor_negative) and its witnesses' murder gossip
(major_negative) were filed under self.level.player_uuid, the world's
singleplayer UUID. Trading reads the reputation under the guest's own UUID,
so hurting or killing villagers never raised that player's prices.
Both now go to the player in last_hurt_by_player (the host, or the guest by
connection), as Villager.setLastHurtByMob and
tellWitnessesThatIWasMurdered report the attacking player.
Fixes#3431
Co-Authored-By: Claude Opus 5.5 <[email protected]>
renderOneHandedMap draws the holding arm (renderPlayerArm, turned 10
degrees outwards) and renderTwoHandedMap both arms (renderMapHand), each
with renderPlayerHand, unless the player is invisible. Horizon drew only
the map. first_person::map_arm_poses gives those poses; the bare arm's
pose (player_arm_pose) and the two-handed map's base
(two_handed_map_base) move to first_person.rs so the empty hand and the
map share them. The game keeps both arms of the player models, and
first_person_arm draws either with the skin and renderHand's 0.1 radian
zRot.
Fixes#3136
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crosshair's living target came only from mob_hit, capped at the
3 blocks of entityInteractionRange, so the attack indicator never showed
for a mob a spear could reach at 4 blocks. LocalPlayer.raycastHitResult
first picks along the held item's AttackRange (getClosesetHit: from the
min reach to the max plus the player's movement, boxes grown by the
hitbox margin, stopped by block outlines) and only falls back to the
plain pick on a miss. crosshair_living now does the same, then keeps
Hud's AttackRange.isInRange check.
The spear's stab search (getHitEntitiesAlong) becomes free functions
over mob boxes with a block clip, shared by the stab (collider) and the
crosshair (outline).
Fixes#3209
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by contrib-gate-27. Read in full; matches handleCodeOfConduct and ServerData.acceptedCodeOfConduct. Main merged. Note: adjacent to #2981 in net.rs. union/cg27-3 (main + this wave): fmt, clippy -D warnings, horizon-client/horizon-protocol tests (1395/0) and the fast build pass; lan_e2e sleep, bed-occupied, sleep-villager, sleep-reload, arrows, objects, commands and tab pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
FireworkRocketItem.use answers SUCCESS while the player is fall flying
(it launches the boosting rocket), so Minecraft.startUseItem stops at the
main hand. On a server client Horizon only counted items with a use of
their own as ending the click, so the rocket passed and the off hand's
shield went up too, holding the use until the button let go. On foot the
rocket still passes (PASS), as in vanilla, and the shield still rises.
Fixes#3424
Co-Authored-By: Claude Opus 5.5 <[email protected]>
KeyboardHandler.keyPress sends a held key's repeats to KeyMapping.set and
KeyMapping.click like the press, so a keyboard key bound to key.attack
keeps attacking while it is held. Horizon dropped the repeats of keys
bound to a mouse mapping, so such a key hit once. horizon-ctl's
`key KEY repeat` now keeps a mouse-mapped key held too, as the window's
repeat does.
Fixes#3243
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A jukebox on a server (a multiplayer server, a LAN host, or single
player's own server thread) plays its song for the clients with level
event 1010, whose data is the song's id in the jukebox_song registry.
LevelEventHandler looks the song up in the registry the server sent and
calls playJukeboxSong: the sound, the "Now Playing" overlay and the
dancing parrots. Horizon only made the parrots dance, and started songs
only from its own ticking jukeboxes, which a server's client never has,
so the note particles showed and nothing was heard.
Level event 1010 now starts the song it names, and 1011 stops it
(stopJukeboxSongAndNotifyNearby). A server's client keeps its song until
1011 or until the song ends, instead of dropping it at once because its
own copy of the jukebox isn't playing.
Fixes#3173
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ZombieVillager.tick counts villagerConversionTime down before super.tick
for every living, converting zombie villager; NoAI only skips
serverAiStep. Horizon counted it in customServerAiStep, so a NoAI zombie
villager never finished curing.
Fixes#3429
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player who died in a boat stayed its passenger: after the respawn the
server sat the new player back in the boat (and in the void it died
again). In vanilla the dead body is removed 20 ticks after death
(LivingEntity.tickDeath's remove(KILLED)), and PlayerList.respawn
removes it at once (ServerLevel.removePlayerImmediately); both go
through Entity.setRemoved, which calls stopRiding.
- Guest::tick_death takes the body out of its vehicle at 20 ticks, and
Hub::respawn does it for an earlier respawn (immediate_respawn).
- The client's Respawn after a death (no KEEP_ENTITY_DATA) leaves the
vehicle, as vanilla's new LocalPlayer rides nothing; a dimension
change (keep 3) keeps it.
- In-frame, respawn_player stops the riding of the window's player.
Fixes#3474
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityRenderer.extractShadow skips an entity whose render state is
invisible (Entity.isInvisible). The shadow pass took every mob: the
casters now come from shadows::mob_casters, which leaves out a mob with
the shared invisible flag from a server or the Invisibility effect on a
mob ticked in the window (Mob::is_invisible).
Fixes#3289
Co-Authored-By: Claude Opus 5.5 <[email protected]>
draw_text emitted the eight outline copies and the text on the same
plane in one mesh, so glowing sign text z-fought with its outline and
broke up. Vanilla's TextFeatureRenderer.renderText draws the outline
NORMAL and the text over it with Font.DisplayMode.POLYGON_OFFSET; the
block-entity mesh has no such pass, so an outlined call's text is now
lifted a twentieth of a font pixel towards the reader.
Fixes#3293
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Enderman.hurtServer teleports away from projectile damage only when
!isPassenger(). Horizon's on_damage rejected every projectile, so an
enderman riding a boat or a mob made arrows vanish and shrugged off
snowballs and eggs. A free enderman still dodges.
Fixes#3381
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by contrib-gate-27. Read in full; the sweep matches Player.attack and doSweepAttack (sword, charged, on the ground, not sprinting, the entities in the sweep box). union/cg27-2 (main c6af85743 + this wave): fmt, clippy -D warnings, horizon-client tests (1293/0) and the fast build pass; lan_e2e team-push, pvp, thrown, projectiles, mounts and riding pass on the server thread. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
🤖 Agent: verified by contrib-gate-27. Read in full: the language list now comes from pack.mcmeta without reading every lang file (LanguageManager). union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
Keep German and Simplified Chinese together, retain token-based translation
checks, and refresh Chinese strings for the current launcher.
Co-Authored-By: Codex <[email protected]>
The check left the host 3 blocks east of the guest's spot; pvp's
face_each_other then put the guest inside the hill east of the spawn, where
it suffocated and both attacks hit the wall (server thread, after team-push).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven by contrib-gate-28: main merged (Nightly-only install path: nightly::after_update then prune_old_versions), German strings added; launcher fmt, clippy -D warnings, cargo test 247+45+… passed; isolated-home run of horizon-launcher update pruned v0.2.80/v0.2.88 and the old Nightly, kept the 2 newest releases and the selected one. Orchestrator approved the feature on 10-22 (cg22).
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Test-only. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
* Add the port ledger: vanilla methods next to the Rust functions that port them
tools/port_ledger.py indexes the decompiled sources and the Rust tree into .local/port-ledger/ledger.json and answers where/what/class/coverage queries both ways, with the gaps where a port isn't wired like vanilla (seam, client, class, callee, stale).
* Document the port ledger
* Port ledger: bytecode calls, wider matching, drift, issue and check
Read the game jar's bytecode for exact call targets, constants and ids, and
resolve Rust calls through the receiver's type. Match more methods: same-named
fns in an impl or file named for the class (the impl type must fit the class),
and callees of known pairs linked through the call graph. Leave trivial methods
out of coverage.
New gap kind drift: a port that never names a sound, particle, item, block,
effect or game event its vanilla method uses, or lacks one of its rare numbers.
Seam gaps on the old in-frame path sort last; maths and geometry callees no
longer make seam or callee gaps.
New commands: issue (the classes, Rust code and gaps a bug report points at),
check (what a branch touches, gaps it opens and closes against its base), diff,
and a searchable offline report.html.
* Document the port ledger's new matching, gaps and commands
* Port ledger: class homes, structural matches and the verdict map
Give every vanilla class its Rust home files, weighed from all its matches.
In those files, match a method to the fn that touches the same fields and
reaches the ports of the same callees. Read tools/port_map/*.tsv, where a
person or agent records a verdict for what nothing else finds (port, inline,
missing, skip with a reason), and list the next unanswered batch with todo.
Count field-only constructors as trivial and fold initializer lambdas into
<clinit> and <init>.
* Map the first three batches: animals, inventory screens, world generation
* Document class homes, structural matches and the verdict map
* Map 13 game-logic batches and add map-check
map-check checks verdict-map files against the last build without
rebuilding, so several mappers can check their own rows at once.
* Map the rest of the game logic: classes with no Rust trace, then method batches
* Map client packet handling, entity renderers and models, block renderers
Also let PORT_LEDGER_NO_MAP build without the verdict map, to measure the
automatic matches on their own.
* Map client rendering, particles, sounds and resources
* map-check: accept full Class#method keys
* Map client screens, toasts, recipe book and world screens
* Add a vanilla-parity skill: start parity fixes from the port ledger
* issue: read registry names, so a mooshroom report finds MushroomCow
* Map the remaining vanilla methods: commands, network, packs, advancements and the last ten
* Follow the furnace code to furnace.rs on main; answer Fox.getBreedOffspring
The game no longer runs WebAssembly code mods (the previous commit), so the
workspace drops what made and ran them: horizon-mods' runtime, commands and
test harness (wasmtime, wit-component, wit-parser), the SDK and its WIT
contract and examples, the sandbox's guest crates, and mods/official (the
official mods are native now, one native/<name>/ folder each in horizon-rust/horizon-mods).
horizon-mods keeps content mods, modpacks and the registry index (whose
api_level still reads published .hmod files). `cargo horizon-mod` makes
content mods only: `new` has one template, `build` refuses a crate,
`check` refuses a .hmod with code and points to native mods, and `test`
(the sandbox) is gone. CI no longer installs the wasm32 target.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon's code mods are native now (game/native_hooks.rs, built in by the
launcher). The game drops the WebAssembly runtime and everything around
it: the `mods` feature (wasmtime and cranelift), the mods' hooks, HUD
panels and screens, the item panel and its recipes, input claims, camera
shakes, far terrain, server mods and the extras that moved to WebAssembly
mods. Content mods (blocks, items, data and assets) load as before.
A WebAssembly code mod's row on the Mods page says it doesn't run any more,
or that its native version replaced it. The first world after the update
says once in the chat which stopped and where to get their native
versions; their files stay where they are.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: requeued by contrib-gate-28 after merging main (B9: sprite_cache.rs moved into horizon-client-render with sprites.rs); fmt, clippy -D warnings, horizon-client and horizon-client-render tests pass. Proven earlier by cg27 (sprite_cache_parity OK).
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). world_border.dat lands in the overworld data folder, the border from a main world carries over, and previousPlayerGameType and the .dat_old files are written. Downgrading to an older build keeps the level.dat border: expected. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
A download from Mojang that stopped on the way (the assets' failure is one log line)
leaves the asset store with some of its files, and every completion that copies from
it (the sounds, the languages, #1870) then finds nothing to copy and the game plays
silent and in English. Asking for it again did not help: the folder is found as it is,
so Prepare again never reached Mojang.
`complete_assets` now asks the owner gate again (`download_for_owner`, the one door to
Mojang's files) when Horizon's own store exists and a prepared version has no sounds or
no languages, once per run. The download keeps the files already whole, checked by sha1,
and fetches the rest; a non-owner fetches nothing, which the gate's test now also covers
for a store that lacks assets.
In that download each asset is tried three times and one that keeps failing does not
stop the rest: the log says how many failed and the first one's reason, and the next
run fetches what is left.
Requests had no timeouts, so one connection that went quiet held the whole download
for ever: the agent now gives up after 15 s to connect and 60 s without data.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified by contrib-gate-27. Read in full; checked against Mob.isSunBurnTick, ServerLevel.globalLevelEvent, Projectile.mayInteract/mayBreak and the DecoratedPot/ChorusFlower hits; small fix pushed (sculk mounts from the tick's snapshot). Note: #3104 touches the same minecarts.rs imports, so the second to land needs main merged. union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). lan_e2e mounts PASS on both paths. Parity: CI's run-parity on this head.
The options pages, the mod and shader pack lists, the agent button and the
multiplayer friend screens drew their own labels as literals, so a client with
ru_ru stayed English there. They read `horizon.*` keys now, and the bundled
namespace carries both languages for every one of them.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
`link` copied every file the mirror hands the overlay, so a selection with a
pack on wrote the prepared root out again: 639 MB and 28 181 files here, and a
cold overlay build of 34.4 s against 20.8 s when the same files are hardlinks.
Unix already links them and the overlay only reads them.
A different volume, or a filesystem without hardlinks, still copies.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/golems.rs: main's #3123 marks a built iron golem player-created; this
branch's SUMMONED_ENTITY trigger runs after it, as in
CarvedPumpkinBlock.trySpawnGolem (setPlayerCreated, then spawnGolemInWorld).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`fs::write` hands the bytes to the page cache and returns, so the rename
can be journalled while the data is not: after a power cut `servers.json`
comes back at the right length and full of zeros, which `list()` reads as
no servers at all — the case the rename was meant to prevent.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
🤖 Agent: proven by contrib-gate-28: main merged (kept #2838's anger fields), vanilla IronGolem.canAttack, a built golem spares the player on the server thread and --in-frame (a summoned one fights back), union parity --changed 81/81 PASS, clippy and tests clean.
hud.rs, font.rs, gfx.rs and gfx/entity_batch.rs moved to
horizon-client-render and took this branch's changes with them; the render
harness hashes hud.rs and font.rs at their new paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
render's sprite shape stretched every sprite over its rect, so a mod drawing
tooltip/background and tooltip/frame (or widget/button) in a box of another
size got smeared borders. It now goes through ui::blit_sprite, which does what
GuiGraphicsExtractor.blitSprite does with the sprite's GuiSpriteScaling:
nine-sliced when its .png.mcmeta says nine_slice (47 vanilla GUI sprites),
stretched otherwise. Tile, which no vanilla sprite uses and Horizon doesn't
read yet, still stretches.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A native mod's settings page showed the labels native_settings gave
when Options > Mods was listed, refreshed only after a click on one of
its buttons, so after a language change (or a change in the mod's own
state) the page kept the old labels until clicked. Opening the page now
sends Event::NativeSettings, and the game asks the mod again
(native_settings_refresh, split from native_settings_event).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
sprites.rs, assets.rs and atlas_sources.rs moved to horizon-client-render,
so sprite_cache.rs moves there too (mod sprite_cache; in that crate's lib.rs)
and horizon-client's lib.rs loses its line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChestBlock.combine pairs every ChestBlock, and copper chests in each
weathering state, waxed or not, are ChestBlocks. Horizon placed and drew
them as double chests but only paired chests and trapped chests when
opening a menu, so a double copper chest opened as one 27-slot half. Hoppers,
comparators and the spectator crosshair missed them the same way.
The pairing now lives in horizon_client::containers (is_chest_block,
ChestHalf) and follows DoubleBlockCombiner.combineWithNeigbour: the same
block, the other half's type and the same facing (the facing check was
missing for every chest). The menus, hoppers and the in-frame chest screen
use it.
New harness tools/test_chest_pair_parity.py (VanillaChestPairOracle,
chest_pair_fixture): 3660/3660 cases, 3640/3660 with the old rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Name tags had their own glyph loop: every glyph an 8x8 quad at the line
top and no style. accented.png's 9x12 glyphs (й, Й, Ё...) were squeezed
into the line, bold, italic, underline, strikethrough, obfuscated and the
font style were ignored, the line was centred at -trunc(width / 2) and the
background stopped 8 pixels below the line top.
Vanilla (SubmitNodeCollection.submitNameTag -> TextFeatureRenderer ->
Font.prepareText) draws them like any text, centred at -Font.width / 2,
on a background from y - 1 to y + 9 (PreparedTextBuilder.markBackground).
HudBatch::name_tag lays the line out with text_styled, and names.rs maps
its quads into the world. Fixes#3084.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server opened every block container with its default title
(container.chest, container.shulkerBox...), so renamed shulker boxes,
copper chests and every other container lost their names on the
server thread, on LAN and on dedicated servers. The menu title now
follows BaseContainerBlockEntity.getDisplayName (the custom name, else
the default name) and, for double chests, ChestBlock's
MENU_PROVIDER_COMBINER (right half, then left, then
container.chestDouble).
The shulker box screen always drew "Shulker Box", and the furnace and
crafter screens ignored the server's title; they now draw it like the
other container screens.
New harness tools/test_container_title_parity.py
(VanillaContainerTitleOracle against container_title_fixture, 148
cases), and a lan_e2e "names" check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
stack_tooltip_lines read the potion effect names and the armour trim
lines through Enchantments::translate, which holds en_us only, so a
Russian game showed "Slowness IV" and "Upgrade: Coast Armor Trim" among
Russian lines. They now go through the same translator as the other
tooltip lines: the selected language, then the pinned English names.
Vanilla builds these lines from translatable components
(PotionContents.addPotionTooltip and getPotionDescription,
ArmorTrim.addToTooltip).
Part of #3312 (the raw %2$s: %1$s attribute lines are #3241).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Horizon Fog option only cleared the distance fog when that fog ended
at or beyond the render distance. The Nether's visual/fog_end_distance is
96 blocks, so its fog stayed at any render distance over 6 chunks. Clear
it whenever the camera's fog is atmospheric and no blindness, darkness or
boss world fog applies; the fog of lava, powder snow and water keeps its
limits, as before.
Fixes#3408
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Windows lets a program size a window up to 65535 pixels tall with
SetWindowPos; its client area (984x65496 here) is past the 32768 an RTX
5080 takes, and Renderer::resize handed it to Surface::configure, which
panics ("width and height must be within the maximum supported texture
size"). Renderer::resize now treats a size the swapchain can't take like
a minimized window: it keeps the last size and draws nothing until the
next resize. Renderer::new clamps its first size to the same limit.
Vanilla: Window.onFramebufferResize ignores a size that is not positive
and keeps the last one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShulkerBoxBlockEntity.createMenu opens a ShulkerBoxMenu
(minecraft:shulker_box), whose ShulkerBoxSlots refuse shulker boxes. The
server (single player on its server thread, LAN, dedicated) sent a
generic_9x3 chest menu instead, so it took a shulker box put into a
shulker box, and a vanilla client drew a chest screen.
lan_open_container now sends the block entity's menu type from
horizon_client::containers::block_menu_type; the server already refuses
shulker boxes in that menu, and the client already draws it.
New harness tools/test_storage_menu_parity.py (VanillaStorageMenuOracle,
storage_menu_fixture): 31/31 blocks, 14/31 with the old menu type. New
lan_e2e check shulker_menu.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
containers.rs moved to horizon-client-render (#3186): this branch's SkullSkin fallback (the "Name" key and a plain string profile) is carried over to the moved file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lib.rs: hud, hud_layout, hud_overlays and inventory_effects moved to horizon-client-render; only this branch's pub mod item_activation stays.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: crate split B9: 83 modules move to crates/horizon-client-render (pure move, re-exported under the old paths), announced in #horizon-team at 00:5xZ. Full pr_ready green (2377 workspace tests, parity 356 affected / 0 failed; rustfmt fixed in this head). The PR body lists the open PRs that need a fix and the exact change for each.
StringDecomposer.iterateFormatted stops at a § with no code after it and the
line-break finder goes on with the next part; split_formatted gave up on the
whole text instead. legacy_format had been inserted under
format_translation's doc comment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher), launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40793866687, after the Windows path fix), pr_ready --quick green.
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher), launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40793347222), pr_ready --quick green with the new test shortcuts_start_only_when_play_would.
Instances could be made but never deleted (#horizon-bugs: one stayed
in the list). The instance row gets Delete…: after a confirmation, the
instance's folder (an own instance's, or an imported or registry pack's
work folder) goes to the Trash, where the player can get it back, and
its choices and the selection go with it. Horizon's own instance, the
game's home, stays. Not while a game runs. `instance delete` does the
same from the command line.
An own instance whose folder was removed outside the launcher is
dropped from instances.json when the list is next read, so a new one
of that name starts clean (only when instances/ itself can be read).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main went Nightly-only (#3342): install_tag runs nightly::after_update (it
removes the older Nightlies) and then prunes the old release versions; the
Settings page drops the Nightly row main removed and keeps the pruning switch.
Both sets of new strings are kept in fr.json and ru.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Potion, splash and lingering potion and tipped arrow tooltips printed
durations as m:ss ("Speed (3:00)"); vanilla's StringUtil.formatTickDuration
pads every field to two digits ("Speed (03:00)", "01:00:00" from an hour up).
brewing.rs now has one port of formatTickDuration, with vanilla's floor of
ticks / tick rate, and the inventory effect panel uses it too instead of its
own copy, so the two agree on the same screen. Tooltips still format at
20 TPS until their callers pass the level's rate.
Fixes#3425.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds audio output device selection matching vanilla options.txt format:
- Bind soundDevice option in Settings with serialization roundtrip
- Enumerate output devices and support live stream switching in Audio
- Add SoundDevice option button to Music & Sounds page cycling devices
- Apply audio device changes dynamically on options save and startup
- Unit tests for settings parsing and options UI button cycling
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Save round trip main -> PR -> main -> PR keeps the mobs. Follow-up, not a blocker: vanilla NeutralMob.readPersistentAngerSaveData falls back to the old AngerTime key when anger_end_time is missing, and this reads only anger_end_time, so an angry wolf from an older world calms down. angry_at is kept only for the offline single player. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
Tested on Hyprland 0.57 (always-on HDR, NVIDIA): it reports reference white
203 and 0..1060 nits, so Auto gives Paper White 203 / Peak 1060, and it shows
HDR windows below its desktop's SDR white (sdr_max_luminance 430, which the
protocol doesn't carry): a white browser page is brighter than the game's
white and than a 1000-nit patch in mpv.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A text display's background, shadow and glyphs were coplanar, so they
z-fought (server holograms looked speckled). World text now gets vanilla's
depths (background -0.01, shadow 0, glyph 0.03, bold copy +0.001, lines
+0.01) and text displays draw with TEXT_POLYGON_OFFSET's depth bias.
The new text_display_render harness runs vanilla's TextDisplayRenderer and
Font on baked glyphs and compares every quad; it found and this fixes: § codes
kept across wrapped lines (StringSplitter.splitLines on the raw parts), line
effects starting 1 left at each style run, and no shadow when its colour is 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). lan_e2e thrown and projectiles PASS on both paths. Parity: CI's run-parity on this head.
FireworkRocketEntity.tick checks its move for entities (getHitResultOnMoveVector)
and goes off on the first it meets; a breeze turns it back. Its owner (the player
who launched it from a block, a glide or a crossbow) is credited with what the
blast hurts, as the guest on a server. /summon and saved chunks now load a real
rocket (Life, LifeTime, FireworksItem, ShotAtAngle, Owner, LeftOwner).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by contrib-gate-27. Read in full: the language list is missing when assets/pack.mcmeta is (lang.rs reads languages from it, and horizon-prepare takes it from the asset index); the jar's lang folder always holds en_us.json and deprecated.json, so the old file count never told. Launcher clippy -D warnings is clean, and prepared_versions_get_what_the_asset_store_adds_without_preparing_again passes. CI parity runs on this head.
🤖 Agent: verified by contrib-gate-27. Read in full; checked against ServerPlayer's fall, lava, Nether and explosion tracking; fix pushed: the durability trigger fires only when wear is left (ItemStack.hurtAndBreak). union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
🤖 Agent: verified by contrib-gate-27. Read in full; throws take getKnownMovement (main's rider movement kept). Main merged. union/cg27-2 (main c6af85743 + this wave): fmt, clippy -D warnings, horizon-client tests (1293/0) and the fast build pass; lan_e2e team-push, pvp, thrown, projectiles, mounts and riding pass on the server thread. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
A mob's death loot, its gift table and spawnAtLocation drops (a fox's
berries, a panda's held food, a goat's horn) spawned with a fixed
[0, 0.2, 0] motion, so every item landed stacked at one exact point; the
5-arg ItemEntity(level, x, y, z, stack) constructor throws each one with
a random horizontal push of ±0.1 and 0.2 up, like vanilla. Block drops
(already going popResource's path) unaffected.
Fixes#3144.
Co-Authored-By: JustKarkat's AI agent (ZCode)
🤖 Agent: verified by contrib-gate-27. Read in full; matches AbstractBedBlock.useWithoutItem (OCCUPIED), kickVillagerOutOfBed, checkBedExists and the saved sleeping_pos; its own lan_e2e checks pass. union/cg27-3 (main + this wave): fmt, clippy -D warnings, horizon-client/horizon-protocol tests (1395/0) and the fast build pass; lan_e2e sleep, bed-occupied, sleep-villager, sleep-reload, arrows, objects, commands and tab pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). A saved amplified world generates amplified terrain in new chunks at 3000/3000 (main: normal), screenshots taken. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Parity: CI's run-parity on this head.
The first perf_bench run on Windows: Horizon v0.2.97, vanilla 26.3 and vanilla with Sodium,
Lithium, FerriteCore and ImmediatelyFast, render distances 8, 16 and 32 at 1280x720, three
games each. The reports are in benchmarks/perf. The page next to them gives the setup, the CPU
load and compiler activity during each run (Windows has no load average), and the tables.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 26.3 manifest gives every OS -XX:StackShadowPages=32, and the official launcher passes it.
perf_bench only took the manifest's OS-conditional arguments, so vanilla started without it. On
Windows about half of those starts crash before the title screen: an access violation under
shaderc.dll while the client compiles its shaders, with no hs_err file. Measured on Windows 11
(Ryzen 7 9800X3D, RTX 5080, Java 25.0.5), 8 starts each through run_vanilla: 4 crashed without the
flag, none with it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/perf_bench.py stopped on Windows before or right after the first game:
- the control socket is AF_UNIX at /tmp, which Python on Windows doesn't have; the game
there listens on a localhost TCP port named in a .json endpoint file (control.rs), so
the driver now uses that transport when there are no Unix sockets;
- memory was read with `ps`; Windows uses the process's working set (GetProcessMemoryInfo);
- os.getloadavg doesn't exist; the report leaves load_avg_1m out there, as horizon-bench does;
- hardware() read /etc/os-release and /proc; Windows gets `ver`, the CPU brand string
and the total RAM;
- nvidia-smi reports per-process memory as [N/A] under WDDM, which raised ValueError;
- the binary is horizon-client.exe.
A release binary passed with --build reports its own version (from its first log line)
instead of this checkout's version and commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
The raw 0x01 and NUL bytes in the byte string made git, grep and GitHub treat sprite_cache.rs as a binary file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The renderer is made anew a few times while the game starts, and each asked
the compositor again and logged the same line; a slider dragged in the
desktop's HDR settings logged a line per step. Now a line comes only when the
values changed, and a change within a second of the last line waits until the
values have held for a second, so only the value they settled on is logged.
The values themselves still apply at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A cold 1024-chunk LOD loaded about a quarter sooner with 6 sampling threads than with 3. The extra slots only open once the nearby chunks are in, and the far threads now run behind the server like the chunk workers, so frames and ticks keep their cores.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla's debug screen draws a column in each top corner
(DebugScreenOverlay.extractLines, left- and right-aligned), but the
helper only reported the top left as taken, so a native mod's box in the
top right was drawn under F3's right column. It now reports both top
corners while F3 is open, and a bottom corner while a debug chart is
drawn there (frame and tick times, bandwidth and ping, the lightmap
view), from the new DebugCharts::corners, which follows draw().
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameRenderer.takeAutoScreenshot saves only when countRenderedSections() > 10
and every section is rendered. A window that joined its server thread
had nothing pending before its first chunks, so the icon was only sky.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
They use only modules already in the render crate (zip, snbt, settings,
sound_events), and open PRs call them from options_ui (#3325: datapack,
#3355: audio). Pure move: cpal and symphonia and the `audio` feature
move with them (horizon-client's `audio` forwards to it), and
sound_engine's test backend is also built for the `test-support` feature
that horizon-client's tests turn on. audio_harness and
docs/architecture.md point at the new paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #3011 the window joins the server thread in poll_integrated_start,
where open_integrated's folder is no longer in scope: Starting carries it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). A world with DataVersion 3955 shows the backup prompt (vanilla text and buttons). Create Backup and Load writes backups/<date>_W.zip and opens the world, now at 5023. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
OriginWorld was inserted between World's doc comment and World, so the
comment documented both structs as one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mount events looked every mob up again in the mob list, once per mob
and tick; the snapshot the loop walks already reads the mob.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemStack.hurtAndBreak calls applyDamage, and so ItemDurabilityTrigger,
only when processDurabilityChange leaves damage: a hit Unbreaking saves
fires nothing. One helper, Game::wear, takes the loss and fires the
trigger for every call site.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: hook points v2, part 2/2 (the item panel and recipe pages for native mods, NATIVE_HOOKS_API 2), rebased onto main after #3434. pr_ready --quick green on 11f22e2 (clippy, 1289 client tests); full pr_ready with parity green on the pre-rebase head (33/33 selected). The native Recipes mod's proof passes on it (horizon-mods#24).
Frogs had no long jump on the server, so they only ever walked. Port the
LONG_JUMP activity as one goal: LongJumpToPreferredBlock searches the
9x5x9 box (weighted by squared distance, lily pads and big dripleaf first
half the time) for a spot FrogAi.isAcceptableLandingSpot accepts and an
8-block path cannot reach, faces it for 40 ticks and leaps along the
LongJumpUtil arc (now with its collision check); LongJumpMidJump holds the
LONG_JUMPING pose without friction until the frog lands. The
LONG_JUMP_COOLDOWN_TICKS memory counts down and is reset as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
The log config and old versions' asset folder were joined from
"a/b" strings; on Windows their tests compared those mixed
separators with paths joined part by part. Join them part by part.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: crate split B8: spawner, trial spawner and beehive behaviour become extension traits (pure move). Main merged, including #3064 and #2952 (their new methods now live on the traits). Full pr_ready green: 1286 tests, parity 82 affected / 0 failed.
The server broadcast LivingEntity.hurtServer's damage event for mobs only,
so in multiplayer and on LAN (and single player's server thread) nobody saw
another player turn red when hit. The LAN flush now sends it for the host
and each guest whose hurt time restarted (a hit that was not blocked by a
shield), to the players around and the hurt player itself. An in-frame
host's models of its guests also carry their hurt time.
Fixes#2827.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A server's Join started the game while its version was still being
downloaded and prepared for the first time: a white window opened and
the download went on. The button only looked disabled while a game
ran, and its click (like a world's Play, a friend's code and a pack's
Play) reached `play()`, which never asked whether a preparation was
under way or the version was prepared.
Every quick start now asks what Play asks (`starts_now`, from
`play_press`): prepared, no preparation running, no game running or
starting. `play()` and `play_pack` refuse otherwise, and the buttons
are drawn from the same rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A riding player skips the movement tick, which held the swimming
update, so a player who boarded a boat while swimming kept the swimming
pose: eye 0.4 above the feet, the camera at the waterline and the model
lying under the boat.
Vanilla's Entity.rideTick runs Player.tick for a passenger:
Entity.updateSwimming clears the swim (it needs !isPassenger()) and
Player.tick ends with updatePlayerPose. The riding branch of the remote
tick now runs the same two steps (MotionWorld::passenger_pose_tick).
Fixes#3308
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (single player included) a menu click is predicted by the
menu engine and sent, and what the prediction dropped was ignored, so
Q over a slot threw the item without the arm swing. Vanilla's
AbstractContainerMenu.doClick drops with Prediction.PREDICTED, and
LivingEntity.drop then swings the main hand on the client itself; the
server shows that swing only to the other players.
The prediction loop moves to predict_clicks, which also says whether a
click dropped something; remote_menu_ops swings the main hand then.
Fixes#3357
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
The item panel beside container screens and its recipe pages move to
game/native_panel.rs and game/native_panel_recipes.rs, built in every
build, with their state in Game::panel and native types (NativeStack,
NativeRecipeView, NativeRecipeLayout, NativePanelEvent). A native mod
shows them with native_panel_items and native_panel_recipes and hears
the player's clicks, searches, page turns and keys over items through
the new hook point native_panel_event: NATIVE_HOOKS_API is 2.
A WebAssembly mod's panel feeds the same state through game/mod_panel.rs
(feature `mods`) and hears its events as before; a native mod's panel
wins when both show one. Players see no change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
kill_dragon only dropped the entity, so the fight, still not killed,
spawned a fresh dragon on its next tick and never opened the exit
portal. EnderDragon.kill calls dragonFight.setDragonKilled after
removing the dragon: call dragon_killed there too, in the End.
The command execution corpus gets an End group that kills the fight's
dragon and probes the dragon, the exit portal and the egg.
Fixes#3319
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The walk animation counted vertical movement while flying or gliding.
Vanilla's LivingEntity.calculateEntityAnimation only counts it for
omnidirectional air movers, and Entity.omnidirectionalAirMover is false
for every player, so only the horizontal distance walks the model.
Fixes#3302
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (single player's server thread included) the creative screen's
Survival Inventory tab sent its clicks as ContainerClick, so the server held
the picked-up stack while the Destroy Item slot only cleared the client's:
the stack came back on the next click or on close, and moving a stack
doubled it. CreativeModeInventoryScreen.slotClicked clicks the player's own
InventoryMenu and sends each changed slot (SetCreativeModeSlot); Horizon now
does the same.
- remote_click: the creative screen clicks its own menu, never a ContainerClick;
a Shift double click there quick-moves locally too.
- Q / Ctrl+Q over a slot of the tab sends the thrown stack
(handleCreativeModeItemDrop) instead of losing it.
- The creative slot sync covers the armour slots (InventoryMenu 5..=8).
- The server marks a creative-set slot as sent (InventoryMenu.setRemoteSlot),
so it no longer echoes it back over a newer click, and the client marks only
the slots a server packet changed as synced.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`item_tooltip::name_line` stripped only a `minecraft:` prefix and looked
up `item.minecraft.<path>` and `block.minecraft.<path>`. A content mod's
item (`ruby:ruby`) never found its name: the name above the hotbar
showed the raw id ("ruby:ruby"), and the recipe book and creative
searches matched that instead of the item's name.
The keys are now `Util.makeDescriptionId`'s, in the item's own
namespace with `/` as `.`: `item.<namespace>.<path>`, then the block's
for a block item. Without a translation the name falls back to the path
with spaces, as `creative::Names::item` does.
Vanilla: Item.getDescriptionId, Item.Properties (ITEM_DESCRIPTION_ID,
BLOCK_DESCRIPTION_ID), Util.makeDescriptionId.
Fixes#3396
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An enchanted shield with a banner turned white: after the patterns, the
renderer drew the shield base again with the foil for vanilla's
`patternedShieldGlint` pass. In vanilla that pass draws the glint alone
(`RenderPipelines.GLINT` with only the glint texture), but a foil quad
here is its texture with the glint over it, so the plain base covered the
base colour and every pattern.
Every layer of a patterned shield now carries the foil instead, and the
extra pass is gone. The GUI adds the glint masked by each layer's alpha
and the world shader composites it per layer, so each pixel ends with
the glint once over the layers below it, as vanilla's single glint pass
over the finished plate gives.
Vanilla: ShieldSpecialRenderer.submit, BannerRenderer.submitPatterns,
RenderTypes.patternedShieldGlint.
Fixes#3407
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A spectator's right click on a chest did nothing: the client predicted
the use like any player's, and the server dropped a spectator's
UseItemOn. Follow vanilla instead:
- MultiPlayerGameMode.performUseItemOn: a spectator's use goes to the
server, predicts nothing and ends the click (CONSUME, no swing).
- ServerPlayerGameMode.useItemOn: the server opens the block's menu
(BlockState.getMenuProvider) for a spectator, with no lid, sound,
game event, stat or piglin anger. Chests, trapped chests, barrels,
shulker boxes, hoppers, dispensers, droppers, furnaces, the brewing
stand and the crafter open; a blocked chest still doesn't.
- ServerGamePacketListenerImpl.handleContainerClick: a spectator's
click changes nothing, and sendAllDataToRemote now takes back what
the client predicted.
Fixes#3414
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Parity: CI's run-parity on this head.
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Main merged in (95ecc9d77): armour stands keep main's layer_color. Parity: CI's run-parity on this head.
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Parity: CI's run-parity on this head.
The resolve looked the history up at this frame's jittered position, so a
still camera wobbled in an 8-frame pattern (twice the frame-to-frame change
of TAA off in a still view), worse when upscaling widens the jitter. The
history is unjittered now: each output pixel looks it up at its own centre,
takes the world pixel whose jittered sample lands nearest that centre, and
scales the frame's 10% share by a Gaussian of the distance, so frames whose
sample lands far away barely move it.
The graphics screen drops the Upscaling row while Anti-aliasing is not TAA.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An Upscaling button under Anti-aliasing (`upscaling` in horizon-graphics.txt,
Native by default). The world renders 1.5x, 1.7x, 2x or 3x smaller along each
axis than the resolution scale's size (FSR's and DLSS's ratios) and TAA resolves
into a history at full size, each output pixel taking the world pixel it falls
in; the jitter runs over 8 phases per output pixel. Picking a ratio turns TAA
on, leaving TAA goes back to Native, and nothing is upscaled on the title
screen or under a shader pack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
cmd reads its own command line and Rust quotes only paths with spaces, so a
path with `&` or `^` split or changed the mklink call and that player fell back
to the slow mirror. The overlay test now runs under a path with `&` and checks
the untouched directory is a link, not a copy.
Anti-aliasing now cycles Off, FXAA and TAA (`taa` in horizon-graphics.txt, off by
default). The world's projection is jittered over a Halton (2, 3) sequence of 8
frames; the resolve reprojects each pixel through the depth buffer and the
camera's motion since the last frame, samples the Rgba16Float history with a
5-tap Catmull-Rom, clamps it to the frame's 3x3 neighbourhood and blends 10% of
the new frame in. It runs at the resolution scale, before the hand and HUD, and
stays off under a shader pack.
Motion vectors come from the camera alone: mobs and other moving things lean on
the clamp. Per-object motion is the follow-up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Projectile.shootFromRotation adds source.getKnownMovement() to the
projectile, the vertical part only when the shooter is off the ground. For
a ServerPlayer that is lastKnownClientMovement: the move the client last
sent (handlePlayerKnownMovement), reset to zero by handleClientTickEnd when
no move came that tick.
Horizon added the player's PlayerMotion velocity instead. On the server
(single player's server thread, LAN, dedicated) a guest's velocity only
holds knockback, not the player's own movement, so an arrow shot while
jumping or running lost the shooter's motion and flew lower and shorter
than vanilla's.
Guests now keep known_movement: set on each accepted move, zeroed at a
ClientTickEnd without one. enter_guest puts it in Game::known_movement for
the turn and leave_guest restores the outer turn's, so nested turns keep
their own. Bows, tridents, snowballs/eggs/ender pearls, thrown potions
and wind charges take known_player_movement(), which the spear already
used. The spear and the per-tick using/digging turn no longer copy the
last of the tick's moves themselves, which was zero once those moves had
been consumed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: hook points v2, part 1/2 (recipe tables and helpers for native mods). pr_ready green on the stack's head 1dba99f: clippy, 1286 client tests, parity 33/33 selected; the native Recipes mod builds on it and its proof passes. Merging it queues part 2, #3435.
test_datapack_worldgen_parity.py runs the whole-chunk oracle and
chunk_dump with HORIZON_DATAPACKS: the oracle selects every pack of the
folder after vanilla through vanilla's pack repository, chunk_dump reads
them through worldgen::overrides into GeneratorData::load_with.
tools/fixtures/datapacks holds Horizon's own pack (MIT), overriding a
noise, a density function, the overworld noise settings, a placed
feature and a biome tag, and adding a noise and two density functions.
Each site must match vanilla with the pack and differ from Horizon
without it. HORIZON_PARITY_DATAPACKS compares another folder of packs.
docs/world-generation.md describes what a pack changes and what Horizon
leaves out; the README lists world generation among what data packs do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The enabled packs of <world>/datapacks (in datapacks.json's order) now
reach the chunk generator: their worldgen files replace and join the
prepared bundles, and the log lists what Horizon leaves out (new biomes,
dimension types, folders the generator does not read, noise taller than
the level). Generator data is cached per set of pack entries, beside the
plain bundle the world screens preload. datapack::enabled_ids reads which
packs are on, as /datapack does.
A world without data packs reads the same bundle as before. Sapling
growth and bone meal still place vanilla's features.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WorldgenOverrides maps a data pack's worldgen files (noise, density
functions, noise settings, material rules and conditions, carvers,
features, placed features, structures, sets, pools, processor lists,
biomes, level stems) to the prepared bundle each joins, the highest pack
winning, and its block, fluid and biome tag files to the tag bundles in
pack order, as vanilla builds its registries from the enabled packs.
Carvers and features use 26.3's folder names (worldgen/carver,
worldgen/feature). A pack's new biome is left out, with the tag entries
and climate points that name it. Dimension types are reported, not
applied: each level keeps its vanilla build height for now.
Nothing calls it yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GeneratorData::load_with reads every bundle through WorldgenOverrides
(the loader threads included), so a data pack's noises, density
functions, noise settings, rules, carvers, features, structures and
biome changes reach generation. Pack tag files join the prepared tags;
a tag that names something missing is left out with a warning, as
vanilla's TagLoader does, where the prepared tags alone must still all
resolve. A pack's level stem replaces the world preset's dimension.
Noise settings that reach past the dimension's build height fill only
the part inside it (NoiseSettings.clampToHeightAccessor), and the
generator says so, since every level keeps its vanilla height for now.
load is load_with with no packs, so vanilla generation is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The renderer keeps the window's surface feedback and looks at it every frame
(only events winit already read; no waiting). On preferred_changed it asks
again, and the game applies its settings, so Auto follows a move to another
output or a recalibrated desktop without a restart. Docs: the Auto values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Against a vanilla 26.3 server (127.0.0.1, offline): moving a renamed/unbreakable/lored sword and a shulker box holding 5 diamonds in the creative inventory kept every component and did not kick. Parity: CI's run-parity on this head.
session_lock_is_exclusive read session.lock while its lock was held.
Windows file locks are mandatory, so that read failed with OS error 33.
It now reads the file once the first lock is dropped.
port_publish wrote the store path as str(path) with the home replaced by
"~", which kept Windows backslashes (~\code\...). The path under the home
is now written as ~/ plus its POSIX form, as on Linux. The temporary body
file is opened as UTF-8, so the "→" in a facts line no longer fails to
encode in the Windows code page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A finished piston's block is drawn over the chunk mesh until that mesh
shows it, but it was lit with the light stored at the moving piston's own
cell. Once the placed block stands there and the edit is relit, that cell
is opaque and holds 0: the pushed or pulled block and the piston base
flashed black for the frames until the mesh came up (#3002).
Vanilla's PistonHeadRenderer.createMovingBlock gives each block a
MovingBlockRenderState (the lone block at the cell it moves from, the
level's light engine), and MovingBlockFeatureRenderer tesselates it with
ModelBlockRenderer.tesselateBlock: in tesselateFlat each quad reads
BlockModelLighter.getLightCoords at its cull-face neighbour, or
prepareQuadFlat's position (the neighbour when cubic, else the block).
Moving blocks now follow that through MeshTables::flat_light, the chunk
mesher's own flat-light rules; a finished one is lit as the block placed
at its cell, which it stands in for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
session_lock_is_exclusive read session.lock while its lock was held.
Windows file locks are mandatory, so that read failed with OS error 33.
It now reads the file once the first lock is dropped.
port_publish wrote the store path as str(path) with the home replaced by
"~", which kept Windows backslashes (~\code\...). The path under the home
is now written as ~/ plus its POSIX form, as on Linux. The temporary body
file is opened as UTF-8, so the "→" in a facts line no longer fails to
encode in the Windows code page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#3064's TrialConfig::encode and TrialSpawner::override_entity become
methods of TrialConfigExt and TrialSpawnerExt; game/spawner.rs (#3064)
and lan/chunks.rs (#2952) import the extension traits they now need.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Paper White 0 is Auto, the new default: where the system puts SDR white (on
Wayland the compositor's reference white, 203 nits in HDR output's scale), else
203. Both Auto values come from one report the renderer makes at surface open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Python's signal module has no SIGHUP on Windows, so exit_on_termination
raised AttributeError and every parity.py command (--list, --changed,
--status, and pr_ready's local harness run) stopped before doing anything.
SIGTERM is still caught everywhere, SIGHUP wherever the platform has it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's SpriteContents mipmaps the animation's original image, every
frame of the sheet at once (byMipLevel = {originalImage}, then
MipmapGenerator.generateMipLevels), and AnimatedTexture uploads each frame
from that chain at frameX/frameY >> level. Horizon mipmapped each frame on
its own, so solidify and each level's alpha-to-coverage scale were the
frame's. A 16px frame has a single texel square at its 2x2 level: its
coverage there is 0 or a multiple of 1/16, and the bisection could settle
on a scale that left the whole level under the cutout. Kelp frames 0 and 18
did, so far kelp vanished every time the animation reached them (only with
mipmaps on, as reported).
source_chains stacks the frames into one sheet, runs mipmap::generate over
it and cuts each level back into per-frame chains, so every frame shares
the sheet's coverage scale. A frame alone (source_chain) is unchanged.
Fixes#1703. Fixes#3073.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's #2952 added banner, vault and trial spawner arms to lan::chunks::entity_tag next to this branch's brushable and shelf arms; both stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Direct Connection, Add/Edit Server, Join a Friend, Connection Lost, the
death screen and the sign editor's Done button placed their widgets at
width / 2.0 - 100 and height / 4.0 + N in floats. Vanilla's Screen
width and height are ints, so `this.height / 4` drops the remainder;
Horizon kept it, and whenever the GUI height was not a multiple of 4 (a
maximized or tall window: 1009 / 4 = 253 GUI pixels) or the width was
odd, buttons, boxes and centred text landed on a half or quarter GUI
pixel. At scale 3 that is 1.5 device pixels: borders came out thinner on
one side and glyph rows uneven (#3362).
The positions now use int halves and quarters as DirectJoinServerScreen,
ManageServerScreen, DeathScreen and AbstractSignEditScreen do, and
centred text is width / 2 - font.width / 2 (GuiGraphicsExtractor
.centeredText).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PistonMovingBlockEntity.saveAdditional stores facing through
Direction.LEGACY_ID_CODEC, Codec.BYTE of get3DDataValue. The block-entity
canonicaliser rewrote it as an int too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Peak Brightness 0 is Auto, the new default: the peak the system reports for
the display (on Wayland, the surface's preferred image description, in HDR
output's scale), else 1000. The slider's lowest step is Auto; 200..2000 stay
manual.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The item and recipe tables WebAssembly mods read through capability
`recipes` move to game/native_recipes.rs, built in every build (no `mods`
feature), as native types (NativeRecipes, NativeRecipe, NativeItem,
NativeRecipeStack, NativeRecipeKind). Native mods get helpers for them:
native_recipes, native_known_recipes, native_place_recipe and native_give,
refused as recipes.place and recipes.give are. WebAssembly mods read the
same tables, converted once to the runtime's types.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a Java instance selected, Play builds its game off the window's
thread (`backend::java_game`: the owner gate, the version and Java
downloaded the first time, their lines in the log), then starts it as
the Horizon client starts. No account that owns the game opens the
sign-in screen, as for Horizon. Joining a friend and quick play stay
Horizon's.
The Play page names the instance's Minecraft version (a Game row in
place of Horizon's resources and Minecraft folder), needs no Horizon
version, and the sidebar shows the version. An old version's logged
session line never reaches the window.
The translations test also reads strings rustfmt moved to the line
after `tr(`/`trf(`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's f5_reloads_the_server_list (4c4fcedf4) came after this branch: its first Multiplayer click now opens SafetyScreen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`horizon-launcher play --instance <java instance>` starts its Minecraft:
Java Edition. Past the owner gate itself (no developer option), the
version is downloaded from Mojang the first time, the Java it needs is
the instance's own, one found here or Mojang's (downloaded), and the
game starts in the instance's folder as the account that owns it. With
no answer from the services, an owner plays with its last token.
Options only Horizon has (--version, --world, --creative, --join,
--server) are refused for Java instances; --arg passes through. The
window's Play still refuses them and names this command.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player's cape texture was looked up and downloaded, then thrown away: nothing kept the image
and nothing drew it. Now:
- Cape downloads go into the skin slots as `capes/<sha1>` (SkinManager's cape cache), a 64x32
texture in the top half of a 64x64 slot; an evicted one uploads again from memory or the disk
cache. A larger cape is scaled to 64x32, the older 22x17 fits as it is.
- `PlayerSkin.cape` reaches the player models (other players and the local one), which load
`player#cape` (PlayerCapeModel).
- CapeLayer draws it: no cape under an elytra's wings, moved out over a chestplate, hidden with
the Cape skin-customisation option (the local player). PlayerCapeModel.setupAnim turns the cape
by `ModelPart.rotateBy` with AvatarRenderer.extractCapeState's flap and leans, from
ClientAvatarState's cloak (moveCloak: a quarter of the way each tick, snapping from 10 blocks),
its bob and walked distance; other players tick theirs as RemotePlayer does.
- WingsLayer.getPlayerElytraTexture: a player with a cape wears it on the elytra.
- `HORIZON_OFFLINE_CAPE`, which the launcher sets for an offline name, is the local player's
cape when its profile has none.
Other players' Cape option (Avatar.DATA_PLAYER_MODE_CUSTOMISATION) is left to #2971, which
tracks it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main added the pack prompt and server dialogs to the screens that stay open when the window loses focus;
they move into pause_if_inactive with the rest of that check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityArgument.listSuggestions adds SharedSuggestionProvider.getSelectedEntities
to the player names unless the argument takes players only;
ClientSuggestionProvider.getSelectedEntities is the UUID of the entity under
the crosshair. The chat screen now fills Registries::selected_entities from
the nearest entity hit before any block.
For the UUID to name the entity back, two server-side gaps are closed:
- the entity tracker sent a made-up UUID (entity_uuid) in AddEntity; it now
sends the entity's own UUID (ClientboundAddEntityPacket: entity.getUUID()),
assigned on first tracking;
- a UUID in an entity argument also set playerName, which findEntities checks
first among players, so `/tp <uuid> ...` found nothing. parseNameOrUUID
sets entityUUID alone.
The chat_screen and command_parse oracles now return a selected entity from
getSelectedEntities, and the fixtures read it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
ClientPacketListener.handleEntityEvent 35 now calls
LocalPlayer.displayItemActivation(findTotem(player)) for the player's own
event. ItemActivation (40 ticks, two random offsets) ticks in
GameRenderer.tick and resets in GameRenderer.resetData.
ScreenEffectRenderer.renderItemActivationAnimation draws it in the hand
pass with a FIXED item at full brightness, ITEMS_3D lighting and no
camera transform or bob. Only F1 hides it.
Fixes#3142.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A stew taken from the creative inventory gave no effect when eaten and
its tooltip listed none (#3137). The protocol read the
`suspicious_stew_effects` component past without decoding it, so the
creative client's `SetCreativeModeSlot` sent the server a bare stew and
the server's copy had nothing for `SuspiciousStewEffects.onConsume` to
apply. Decode and encode it (`Entry.STREAM_CODEC`: mob effect id,
duration) both ways, and add `SuspiciousStewEffects.addToTooltip`'s
creative-only effect lines (`PotionContents.addPotionTooltip`).
Fixes#3137.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
- Moving blocks are tinted per face with the chunk mesher's block colours
(MeshTables::block_tints), not the terrain particle tint, which is none
for grass blocks: a pushed grass block was drawn gray.
- PistonMovingBlockEntity is saved with its chunk as vanilla's
minecraft:piston block entity and loaded back into the level's moving
pistons, so blocks caught mid-move finish it after a rejoin instead of
leaving an invisible, solid moving_piston (#3310).
- A server client drops its moving piston when the server changes that
block (LevelChunk.setBlockState removes the block entity), instead of
animating it for five more death ticks over the placed block, black and
z-fighting (#3002).
- A finished piston stays drawn until the chunk mesh shows its block, and
no longer, so no frame shows neither.
Fixes#3310Fixes#3002
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ZombieVillager.mobInteract records the interacting player as the
conversion starter and finishConversion credits the player
level.getPlayerByUUID finds: the ZOMBIE_VILLAGER_CURED gossip and the
cured_zombie_villager trigger. Horizon used the server Game's own
level.player_uuid for both, so on the server thread (and for LAN guests)
the gossip went to a UUID nobody trades as and the player got no
discount and no Zombie Doctor (#3306).
The starter is now the acting player, and the cure credits the host or
the placed guest in this level with that UUID. lan_e2e gains a
cure-discount check: host and guest each cure one and pay 14 wheat
instead of 20.
Fixes#3306.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A mod screen's `list` drew one row more than its frame holds (the row a
scroll shows in part), uncut, so without scrolling the 7th row of a long
list sat below the frame, over the screen. As vanilla's
AbstractSelectionList, which draws its entries between enableScissor and
disableScissor, the rows (with their hover and selection marks) are now cut
at the frame's inside: the partly shown row is drawn in part. Fixes#3237.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client read InstrumentComponent past without keeping it, so every goat
horn a server sent arrived with no minecraft:instrument and its tooltip fell
back to "Ponder" (#3181). Decode the holder like ArmorTrim's: a reference
becomes Component::Instrument(registry id) and is named from the synced
minecraft:instrument registry in item_sync. A direct instrument stays Raw.
The LAN host and creative client send it back by the same id.
The item wire harness gains an "instrument registry" fixture, and its probe
treats Instrument like Trim (a byte-exact id, not a lossy type), so the
round trip is checked byte for byte.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
levzzz5154's update (optional public visibility for support modules); bundles that
pin their own mixins revision keep it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server never sent DATA_CUSTOM_NAME or DATA_CUSTOM_NAME_VISIBLE for an
armor stand, so hologram text didn't show for its clients: single player on
its server thread, LAN guests and dedicated servers. Both are sent now, and go
back to none / false when /data takes them away.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntityRenderer.submit gives an invisible stand no body render type but
still submits its layers: held items, the custom head and the armour. The
renderer skipped the whole stand instead, so floating heads, item displays and
armour on invisible stands never showed, in-frame and on any server that sends
the invisible flag (vanilla, Paper, Minestom). Only the wooden body is skipped now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- docs/datapack-upgrade.md: how to run `horizon-port-pack
upgrade-datapack`, every rewrite and how it is checked, and what is not
rewritten; linked from docs/README.md and the command's help.
- tools/datapack_upgrade_rosetta.py writes vanilla 26.2's own worldgen out
as a pack, upgrades it and compares it with vanilla 26.3's file by file:
registries whose format changed and whose content did not must come out
identical.
- tools/datapack_upgrade_fidelity.py starts vanilla's 26.2 and 26.3
dedicated servers on the same seed, the first with a pack and the second
with its upgrade, force loads the same chunks and compares the saved
blocks; without packs it measures what 26.3 itself changed.
Both need the 26.2 server jar the player downloaded; nothing is fetched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fourth part of the 26.2 to 26.3 data pack upgrade.
- Configured features move to `feature/` with `config` unwrapped, block
state providers take their 26.3 names (a `simple_state_provider` is its
state), and nested and inline placed features are upgraded with them.
Feature types 26.3 replaced by compositions of other features are listed
in the report and left as they are.
- Placement `random_offset` becomes `offset` with one provider per axis
(x and z both from `xz_spread`).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The third part of the 26.2 to 26.3 data pack upgrade: noise settings in
26.3's form, with the parts 26.2 hard-coded written out as data.
- The beardifier, which 26.2's NoiseChunk added, is added to the final
density.
- The aquifer router fields move to `aquifers`, whose `exclusion` spells
out 26.2's `isDeepDarkRegion` (erosion < -0.225 and depth > 0.9) from
the pack's own erosion and depth; `surface_level` is the preliminary
surface, and `chunk_surface_level` its per-chunk 16 x 1 interpolation.
- 26.2's ore veinifier becomes two `ore_vein` material rules (copper,
iron) with OreVeinifier's heights, thresholds, edge roundoff, solidness,
richness and gap, ahead of the surface rule: 26.2 placed veins while
filling the noise and its surface pass only replaced the default block.
The rule is written as a named `material_rule` under the settings' id.
- The climate functions are named, and the spawn target refers to them by
id (`depth` and `offset` dropped, as vanilla did).
- `size_horizontal`/`size_vertical` are dropped: each `interpolated` holds
its cell.
- Inline noise settings in dimensions and world presets get the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The second part of the 26.2 to 26.3 data pack upgrade: named density
functions in 26.3's form.
- Inputs are named `input`/`left`/`right`; `invert` is `reciprocal`,
`y_clamped_gradient` a `gradient` on y, `shifted_noise` a `noise` with
shifts, `end_islands` `end_outer_islands`, and `constant` its number.
- 26.2's `flat_cache` held each column's value at y = 0, so it becomes a
`cache` around a `slice` at y = 0 when its input depends on y (through
the pack's own functions); 26.2's other caches only memoise in 26.3.
- `interpolated` takes its cell from the noise settings whose router
reaches it (`size_horizontal * 4`, `size_vertical * 4`), which 26.2 read
from the settings, or vanilla's overworld cell when none does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SavedLodSampler::grid put saved chunks over every far tile of 16 blocks
a column or finer: for each column it opened the region file and
decoded the whole saved chunk, and each sampling thread kept only 128.
In a save with many thousands of chunks, the far view stalled for many
minutes after its first one.
- SaveDir keeps which chunks each region file holds (10 s), so columns
over unsaved ground no longer open region files.
- Decoded saved chunks are shared by the sampling threads (1024, 30 s).
- With Reduced far detail, tiles coarser than 4 blocks a column (about
1 km out and beyond) keep the generator's terrain, which saved chunks
were made from; edits that far away show when their tiles refine on
approach. Full keeps saved chunks up to 16 blocks a column, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The previous two commits trade the exact far tile for speed in wide
cells. They now apply only with Reduced far detail (FarDetail::Reduced,
which already "changes the picture"); Full keeps the exact tiles.
- GeneratorWorker::lod_grid takes `approximate`: one biome per 4x4
columns, the coarser vertical lattice, and density only around the
surface (with shared 2D planes) happen only when it is set. Without it
the tile is sampled as before.
- LodSampler::set_approximate, and Lod::set_reduced_detail sets it for
the sampling threads; tiles sampled after a change follow it.
- lod_grid_bench --approximate samples as Reduced does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lod_grid sampled the density lattice over the whole build height. It now
finds each tile's surface band on a 64-block lattice made of the fine
lattice's points, then samples the fine lattice from 64 below the lowest
surface to 128 above the highest (and above sea level); below the band a
column is one span of the default block. Thin parts above an air sample
of the coarse lattice (floating islands, overhang lips) can be missed.
Most of a far tile's density work is 2D noise (y-fixed slices), which
the two passes would each compute: SamplerContext::with_planes keeps a
y-fixed slice's plane for its x/z lattice, so the band pass reuses the
coarse pass's. Opt-in, and only far sampling turns it on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Approximations for wide far cells, where they are hard to see at their
distance:
- One biome for 4x4 of a tile's columns, the first one asked (its
surface's), in cells up to 64 blocks wide: far cells don't show a biome
change a few blocks down, and climate changes over hundreds of blocks.
Painting asks through the biome zoom, which strays into neighbouring
quarts; per quart, those strays were most of a tile's climate samples.
- The interpolated density on a vertical lattice of 16 blocks for cells 32
blocks wide or more, and of 32 from 128, instead of 8.
Distant tiles' biomes and some surface blocks differ from the per-quart
answers, and in the widest tiles the coarser lattice moves the surface.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 refuses a world generation data pack written for 26.2: the
registries changed format. `horizon-port-pack upgrade-datapack <pack>
(<out> | --world <world>)` reads a pack (zip or folder) and writes it in
26.3's formats, with a report of what it rewrote, what has no faithful 26.3
form and which types it does not know. Each rewrite is the one vanilla's own
data went through between 26.2 and 26.3.
This first part handles the registries that need no context:
- noise parameters: `firstOctave`/`amplitudes` become `base_octave`,
`octave_count`, `amplitude_modifiers` and the base amplitude of
`NormalNoise.createParity`, which keeps 26.2's normalisation;
- block states: `{Name, Properties}` become `{id, properties}`, or the id;
- configured carvers move to `carver/` with `config` unwrapped and the cave
carvers' hard-coded count, thickness and room scale written out;
- biome spawners and spawn probability become 26.3's gameplay attributes;
- structure spawn overrides and the dimension type's bed rule.
Density functions, noise settings and features follow in their own
changes. serde_json gains `float_roundtrip` so the numbers the upgrade
writes back are the ones it read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lod_grid paints eight depths of every far column through the surface
rule, and each depth asked the biome resolver again: a full climate
sample and parameter search per query, though neighbouring columns and
depths share quarts. The tile now keeps the answers per quart
(Biomes::Memo), for painting and for the columns' surface biome.
lod_grid_bench, vanilla seed 1234: surface heights, blocks and biomes
are the same at every level. At level 6 (64 blocks a column) some
painted blocks below the surface differ: the tile's cached sampler
context answers a repeated quart from what it sampled before, so the
order of queries mattered there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Times GeneratorWorker::lod_grid as the client's far terrain calls it (16 x
16 columns a tile) for each tile level, with two checksums: everything the
tiles hold (an exact speedup keeps it) and the surface and water heights
alone (the terrain's shape, which an approximation of materials or biomes
keeps). --dump writes each column's height, block and biome.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three read paths in decoration:
- Speleothem clusters (and large dripstone) scan each column up and down
for the floor and ceiling, up to 512 blocks, one region read per block.
FeatureWorld::scan_column walks a chunk's column directly: an all-air
section is tested once (the predicate is a pure state test), a mixed
one is read without a section lookup per block. Same blocks, same stop.
- OreFeature's any_height_at_least read each column of its square through
a region lookup and a checked heightmap read; now each chunk's part of
the square is read from its heightmap in one pass (any column will do,
so order is free).
- Placement biome checks ran the biome zoom (eight hashed corner
distances) for every position. When the 2x2x2 quarts the zoom picks
from lie in biome sections that each hold one biome, the same one, the
answer is that biome whatever the zoom picks; per-section uniformity is
worked out once per region.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three costs that came before any feature work:
- the biomes present around the chunk sorted every quart of the nine
chunks' columns into a BTreeSet; a bitset gives the same ids in the same
ascending order;
- every updateShape call built and dropped a whole Ticks (tick maps,
deadline index) just to capture the ticks it schedules; capturing
leaves the rest untouched, so the region keeps one;
- the nine stand-in chunks left in the proto chunks while the region
holds their terrain were allocated at full height; one section does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A block read in decoration (and the surface stage's reads) went through
out-of-line calls: the region's chunk lookup, TerrainChunk::block,
TerrainBlocks::block and BlockSection::get, whose io::Result error path
kept it from inlining. They now inline into the feature code (the fast
build has no cross-crate LTO, so the core accessors need #[inline]), and
TerrainBlocks reads the section through an infallible accessor: its index
is a local index, always below 4096. #1790 takes the division out of
PackedStorage::read; this leaves storage alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The surface stage's per-chunk cell cache only proved cells whose eight
quart corners hold one biome; a block in any other cell ran the full
eight-corner zoom mix and a stored lookup again. Now a mixed cell keeps
its corners' jitter (BiomeZoom::corner_jitter, one mix per cell) and
their eight biomes, so each block of the cell only redoes the distance
comparison (BiomeZoom::select_corner, same arithmetic and tie order); the
cell of the previous lookup is kept too, as columns walk down in y. A
test checks the split zoom against quart_at_block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every solid block of every column asks the rule tree's biome conditions
for its biome. Each one cloned and dropped the biome's Arc<str> (an atomic
count all generator threads share) and looked its name up in a string set.
Queries that number their biomes (the generator's) now answer with the
index, and biome conditions keep a bit per index; others compare names as
before. Same answers: the generator's name -> id map is exact.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two lookups in the biome stage:
- the biome ids present around the chunk were gathered by inserting
every quart of the nine neighbour chunks into a BTreeSet; a bitset
gives the same ids in the same ascending order;
- each quart's biome name was looked up in the name -> id map. Names
point into the biome source's parameter list and neighbouring quarts
mostly resolve to the same entry, so the last entry's id is kept and
the map is asked only when the name changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each chunk needs the biome chunks of its 3x3 neighbourhood, built once by
whichever thread gets there first (per-chunk work lock). With several
workers on neighbouring chunks, a thread often blocked on a neighbour
another thread was building while other neighbours were still unbuilt.
The stage now first builds (or reads) every neighbour whose lock is free
(try_lock), then waits for the ones still being built. Each biome chunk
is still built exactly once, deterministically; only the order of the
work changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
couldStructureExistInColumn (jigsaw projection, onTopOfChunkCenter,
temples, mansions) samples the column through createResolverForChunk:
one bulk climate volume of 1 x n x 1 quarts, then each quart from the
bottom. Horizon took one scalar climate sample per quart instead, and a
column with no valid biome walks all of them, so a few such start chunks
held most of the structure stage while neighbours waited on them. The
column is now one prepared volume through the biome stage's resolver
path, as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Structure placement asks the biome of one quart at a time: several
structures test the same quarts of a start chunk, and column checks walk
whole columns when no quart matches. Each answer is a full climate sample
and parameter search. Workers now keep the answers, up to 65536 quarts;
the generator is seed-bound, so they cannot go stale.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
9f1fa0a0a moved the mklink call into junction(), which imports
CommandExt itself; make_link kept its own copy, unused, so the
launcher's clippy with warnings denied fails on Windows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Select World list built its info line from a creative flag only, so a
hardcore world read "Survival Mode" and adventure and spectator worlds read
"Survival Mode" too (#3298). LevelSummary.createInfo uses `gameMode.hardcore`
in CommonColors.RED (-65536) for a hardcore world, else `gameMode.` + the
GameType's name, then commands and the version. WorldEntry now carries the
level's game mode and hardcore flag (both already read from level.dat), and
`world_info` builds that line as styled text. The horizon-ctl JSON keeps its
`creative` / `mode` fields as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Keep the original custom-name component as the sole stored value, deriving plain text for gameplay and rendering its styles in tooltips, held labels, and item links. Honor shadow_color in parsed text and HUD glyph passes. Extend the Java/Rust tooltip parity fixture with styled names; its fixture format builds on PR #1702 by RVZO6.
Co-Authored-By: Codex <[email protected]>
The vanilla runs start `$JAVA_HOME/bin/java`, but the driver mod was
built with whatever `javac` came first on PATH. With JAVA_HOME on a JDK 25
and an older default JDK on PATH (21 on Ubuntu 24.04, for example),
`javac --release 25` fails with "release version 25 not supported" before
any game starts. Use JAVA_HOME's javac when JAVA_HOME is set, as the java
lookups below already do; without it, PATH is used as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
In creative mode, playerWillDestroy on an extended piston head destroys
the base piston without dropping resources, matching vanilla Java Edition
PistonHeadBlock.playerWillDestroy.
Fixes#3317
Parse Minecraft object components into inline spans and draw player heads and atlas sprites with the existing HUD atlas. Resolve head profiles through the skin cache and preserve fallback text for plain output.
Co-Authored-By: Codex <[email protected]>
Rabbits fell back to the generic goal list, and ocelots had no goals
of their own, so neither fled. Rabbit.registerGoals now registers its
three RabbitAvoidEntityGoals (players within 8, wolves within 10,
monsters within 4, never for the killer bunny) with panic at 2.2.
Ocelot.registerGoals plus reassessTrustingGoals gives ocelots
OcelotAvoidEntityGoal (players within 16), OcelotTemptGoal, the leap
and attack goals and their chicken and baby turtle targets.
Not ported yet: rabbit RaidGardenGoal and ClimbOnTopOfPowderSnowGoal,
and ocelot trust, so every ocelot is wild.
Refs #2780
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The More tab's Data Packs button opens vanilla's PackSelectionScreen (the
resource pack screen's port, titled dataPack.title) on a new mcworld-
folder: its packs, with their icons, and the built-in one, re-listed when
the folder changes. Create New World copies the folder's packs into the
world's datapacks/ and writes datapacks.json with the selection, as
CreateWorldScreen.copyTempDataPackDirToNewWorld does. Cancel deletes the
folder. Worlds made without opening the screen are unchanged.
The pack screen parity harness also runs the screen under dataPack.title.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.useItemOn swings on InteractionResult.SUCCESS
(SwingSource.CLIENT). The remote client only swung when the held item
was a block, so opening a chest, barrel, furnace, or similar storage
did not start the arm animation.
Use both rows of the separator textures when tiling headers and footers, including the create-world screen.
Co-Authored-By: Codex (GPT-6.1 Terra) <[email protected]>
A chunk's block entity data carries a beacon's levels and powers but not
its beam sections, so in the default server-thread path and on LAN or
multiplayer clients the sections stayed empty and no beam was drawn.
Tick beacons with ClientLevel.tickBlockEntities' beam pass, without the
effects, sounds and advancement the server owns.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Remember each active beacon's beam sections when its chunk unloads, so
far terrain LOD keeps drawing the beam until the chunk loads again and
its block entity decides whether it is still there.
The cache only runs with far terrain on, and clears on world, dimension
or LOD changes.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
World.modified, inhabited, chest_openers and block_names used std SipHash
on small integer keys, and modified/inhabited rehash as they grow while an
area unloads. Use the existing crate::fast_hash maps; none of their
iterations is order-sensitive (save order comes from the batch list and
ChunkExtras::block_entity_order). chunks_with_block_entities takes the
names map by hasher so the faster map still fits.
BlockEntities::map stays std HashMap: crate::lan::chunks::body takes the
default-hasher map by type and lan/** is outside this change.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
BlockSection kept a HashMap of state ids per section (24 tables and
allocations per proto chunk) and hashed on every worldgen write. Resident
palettes are almost always tiny, and the resident container already scans
them; use the same scan with a one-entry last lookup so a run of one state
is a compare. set/compact behaviour and the palette, counts and free list
are unchanged.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
PackedStorage divided by its per-word width on every read and write, while
PalettedContainer already used an exact multiply-shift. Move MAGIC/locate/
mask into storage.rs and use them from both; only storages past 16,384
entries keep the division path.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
A write looped 27 block offsets through the dirty-section map; all but a
few hashed the same keys. Mark the exact section set instead: one key in
the interior, the adjacent section on a section face, a neighbour column
on a chunk border, and the diagonal where both borders meet (at most eight
keys). The mesher still sees the same 3x3x3 neighbourhood.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
A spear's left click is a STAB swing (SwingAnimationType 2), but every
swing was drawn as a WHACK. Port SpearAnimations.firstPersonAttack,
thirdPersonAttackHand and thirdPersonAttackItem, and keep a swing's type
for mobs and other players (from ClientboundSwingAnimationPacket) and
the local player's model.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/dialog show and /dialog clear ran, printed their feedback and told
nobody: the dialog state they set was never read, and nothing built
ClientboundShowDialogPacket or ClearDialogPacket. DialogCommand's own
ServerPlayer.openDialog/closeDialog send the packet to each target, so
the command now resolves the argument (a minecraft:dialog registry entry
by index, or the inline dialog as sent) and sends it to every target:
the host's own player opens it on the screen, LAN guests get the packet
through the hub.
Co-Authored-By: ZCode <[email protected]>
remote_pushes found teams through mob_teams, which keys entities by UUID
text and the local player by PLAYER_NAME. On a server, players are team
members by profile name (Player.getScoreboardName), so neither lookup hit
and every player pushed the local player, even on teams with
collisionRule never. The server applied no push and corrected the
position, so walking into players jittered (seen on Hypixel).
Use the profile name for the local player and the tab list name for other
players; other entities keep their UUID.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
Paper White and Peak Brightness sat among the display settings, where players
without an HDR display took them for settings of their own display. Display now
has only HDR Settings..., greyed where the display path offers no HDR output (the
renderer tells the settings when it opens its surface, `graphics::offer_hdr`).
It opens Graphics: HDR: the HDR switch, then its sliders, greyed while it is off.
Where HDR isn't offered, `horizon-graphics.txt`'s HDR settings are left as saved
and not used: the renderer already keeps HDR off there, and the screen no longer
opens, so a game folder copied from another computer, or a display swapped for
an SDR one, shows nothing that does nothing; an HDR display brings them back.
The screen leaves room for a calibration step and an in-game shortcut to it
later. horizon-ctl opens it as `options graphics-hdr`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port the production path portability repair developed in Beyond onto original Horizon. Keep stored records and narrowed-root annotations intact; cover legacy local/shared records, changed probes and oracles, and reachability.
Co-Authored-By: OpenAI Codex GPT-6.1 Terra <[email protected]>
Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
Give every vanilla class its Rust home files, weighed from all its matches.
In those files, match a method to the fn that touches the same fields and
reaches the ports of the same callees. Read tools/port_map/*.tsv, where a
person or agent records a verdict for what nothing else finds (port, inline,
missing, skip with a reason), and list the next unanswered batch with todo.
Count field-only constructors as trivial and fold initializer lambdas into
<clinit> and <init>.
80 modules of horizon-client's lib (chunks, meshing, light, streaming, gfx, the HUD,
menus, settings, resource packs, particles, block entity data) and its 8 WGSL shaders move
to crates/horizon-client-render with git mv. Nothing in them reaches gameplay. horizon-client
re-exports each under its old path, so horizon_client::gfx and the rest are unchanged.
Beyond the moves: ParsedModels, lod's Column::reduced/push and the spawner's Next,
Spawner.next and Selector.current become pub; Properties::for_tests sits behind a
test-support feature; crash.rs strips the new crate's path; tools and docs point at the
new paths, and three harnesses that hash every client source hash the new crate too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read the game jar's bytecode for exact call targets, constants and ids, and
resolve Rust calls through the receiver's type. Match more methods: same-named
fns in an impl or file named for the class (the impl type must fit the class),
and callees of known pairs linked through the call graph. Leave trivial methods
out of coverage.
New gap kind drift: a port that never names a sound, particle, item, block,
effect or game event its vanilla method uses, or lacks one of its rare numbers.
Seam gaps on the old in-frame path sort last; maths and geometry callees no
longer make seam or callee gaps.
New commands: issue (the classes, Rust code and gaps a bug report points at),
check (what a branch touches, gaps it opens and closes against its base), diff,
and a searchable offline report.html.
screenshot_name worked the date and time out from the Unix clock by hand, in
UTC; vanilla's Util.getFilenameFormattedDateTime is local time, as Horizon's own
log is, so a screenshot taken at 12:28 in CET was named 11.28 (and near midnight
elsewhere, the day before). The log now also has the full path of a saved
screenshot; vanilla's chat line, which names only the file, is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pixels counted as the world when they matched it darkened evenly (the vignette),
and grey text over the white sun matches that: menu text in front of the sun was
lifted to the peak. Once the world is kept for the last pass, the frame's alpha
is now zeroed; the hand, HUD and menus raise it where they draw (the vignettes
leave it), and after the hand a blend pass scales its alpha down to 1/255. The
last pass lifts nothing covered, keeps the hand at the world's brightness (it's
the player's, not UI), and scales the rest by UI Brightness.
UI Brightness (Display, 100% by default) is the HUD and menus as a share of paper
white, so pure white labels needn't sit at the world's white. At 100% the picture
is as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While the sunrise/sunset glow shows (about 11500-14500 and 22500-500),
sunrise_weight rebuilt the fan's 16 triangles for every pixel of the
fullscreen sky pass, with four sin/cos per triangle. The triangles depend
only on the glow's alpha and the sun's horizon, so they are now built on
the CPU and passed in Globals as normals; the shader's Moller-Trumbore
reduces to dot products, and only triangles whose rim normals change sign
along the ray are tested in full. The weights are unchanged (a test checks
them against the old per-pixel code).
Sky pass at 1920x1080, dawn: Intel UHD 730 10.2 ms -> 1.8 ms (0.6 ms by
day), RTX 3060 0.47 ms -> 0.27 ms.
Fixes#3143
HDR output told the display its content goes down to 0 nits, which only an OLED
shows. Black Level (Display, 0 by default) is the display's darkest black: the
last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ
(the peak stays, the lift fades out by the fourth power above black, so midtones
barely move and 100 nits and up don't), and the HDR metadata gives it as the
minimum luminance. At 0 the picture is exactly as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve the guest's updated hotbar selection when restoring the host
player state after an action. The previous restore sequence discarded
the guest's new selection while keeping the inventory changes.
The held-slot response now uses the selected slot produced by the pick,
so integrated-server and LAN players select the picked block or spawn
egg. Keep the host's selection independent of the guest's selection.
Vanilla reference: ServerGamePacketListenerImpl.tryPickItem,
Minecraft.pickBlockOrEntity, and ClientPacketListener.handleSetHeldSlot.
Fixes#3031
tools/port_ledger.py indexes the decompiled sources and the Rust tree into .local/port-ledger/ledger.json and answers where/what/class/coverage queries both ways, with the gaps where a port isn't wired like vanilla (seam, client, class, callee, stale).
Cancel item-model centering in the vanilla carried-block pose. Add a regression test using point coordinates captured from Minecraft 26.3 CarriedBlockLayer.submit, including a transformed entity root.
Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV tested the updated Linux binary in game and confirmed the fix works.
Co-Authored-By: OpenAI Codex <[email protected]>
A built iron golem was never marked player created, so it targeted its builder. Vanilla sets PlayerCreated on build (saved in NBT) and IronGolem.canAttack refuses players, which every target goal honours.
Render the carried block through the inventory item model pipeline so every quad uses its proper texture and tint source. Remove the single-sprite cube renderer and let the item pipeline center the model.
Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV confirmed the fix works in game.
Co-Authored-By: OpenAI Codex <[email protected]>
Writes a datapack into a world: hdr:gallery (one or two of each family) and
hdr:gallery_full (every block with a light level, in its brightest state), each
alone in dark stone tunnels, after hdr:setup holds the world at midnight with no
weather, mobs or random ticks. To judge and tune how light sources shine in HDR
output (#1441) under one standard; the block list comes from the prepared
resources, so it follows the pinned version.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only a soul lantern needed it, and it lifted less for its light level, not for
its texture: that is better fixed where the lift is worked out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`lacks_languages` counted the files in `assets/minecraft/lang` and took one or none as
"English alone". The client jar carries two there, `en_us.json` and `deprecated.json`,
so a real prepared version never counted as lacking and the languages #1594 copies from
the asset store were never added to one prepared before it, or by a preparer that could
not read the store. The test's fixture held `en_us.json` alone, which hid it.
The list the language screen offers, `pack.mcmeta`, is only in the asset index, so its
absence is what says the languages are missing. The fixture now has `deprecated.json`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
hotbar::save's error was dropped and the action bar showed "Saved hotbar N"
anyway. Log the failure and skip the message, as saveHotbar does by setting
it inside the try.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Game::aim() returns the eyes and the view direction; pick, pick_fluid
and every *_hit go through it instead of rebuilding the pair. A tracked
controller can later stand in for it. No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
save_scoreboard wrote scoreboard.dat and world_border.dat with `let _ =`, so
a full disk or a read-only world lost objectives, scores and the border with
nothing in the log. Log the failure the way command storage and boss bars
already do.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The bright tier's window goes to 0.04-0.28, from a round in the HDR gallery: a soul
lantern's glass (light level 10) now lifts, and the level-15 bright blocks still look
right.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
servers.json was written in place, so an interrupted write left a truncated
list and every server vanished from the sidebar. Write servers.json.tmp and
rename it, as the launcher's other state files do.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
LockFileEx refuses a read through another handle while the lock is held, so
reading session.lock between acquire and drop fails on Windows with error 33.
The exclusivity assertion stays; the bytes are read once the lock is released.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Pearls, snowballs, eggs, splash potions, tridents, wind charges and arrows added the server's stored velocity of the player, which for a guest can hold knockback or a fall from ticks ago, so some throws dove. Projectile.shootFromRotation adds getKnownMovement: the move the client last sent.
str(Path) keeps the host's separators, so a Windows run wrote
`~\code\horizon-parity\...` into the published body while every other line
of that body is the same on every machine. as_posix() makes it stable.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The renderer now builds its camera from `View` (rotation, lens, aspect,
tan(fov/2)) gathered in a `ViewSet`: one view on a flat screen, and the
view culling and back-face removal use. A headset can later supply one
view per eye without touching draw_frame's consumers.
No visible change: the products keep their order (projection * pose *
rotation), and a test pins the screen view to the old formula bit for bit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon had no ElytraOnPlayerSoundInstance, so gliding and diving were silent. The loop starts when the player starts gliding, fades in after a second, and follows the player's whole speed, so a straight dive is the loudest.
Fill the 17 missing Microsoft sign-in, resource setup and Nightly strings.
Scan Rust tokens for multiline translation calls and require shared catalog
keys in every language, including labels translated through widgets or constants.
Co-Authored-By: Codex <[email protected]>
Very bright's window (0.88 to 0.94) was tuned on the sun alone: on a block
only the hottest pixels reached it, and they blew out while the rest
stayed below bright. It now lifts over bright's window with the whole
headroom; the sun keeps its yellow corona near the horizon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.startSleeping sets the bed head's OCCUPIED and stopSleeping
clears it (the foot follows through updateShape):
- Villagers send MobEvent::BedOccupied when they lie down and get up
(brain, hurt, bed gone, kicked out, death), and keep the bed they lie
in as sleeping_pos.
- A player's bed is marked when it lies down (try_sleep) and freed in the
level's tick once nobody sleeps in it any more, which covers waking, the
Leave Bed button, death, a broken bed and a guest leaving; the server's
last save (and closing an in-frame world) frees every player's bed, as
PlayerList.removeAll does.
- AbstractBedBlock.useWithoutItem: an occupied bed wakes the villager in it
(kickVillagerOutOfBed) or says "This bed is occupied".
- LivingEntity's sleeping_pos is saved and loaded, so a villager asleep at
a save is still asleep in its occupied bed when the world opens again.
- The in-frame player gets up when its bed is gone (checkBedExists), and a
client that leaves a server while asleep is no longer stuck in bed.
lan_e2e: bed-occupied and sleep-reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The guest uses a bed a villager has lain down in: the villager gets up and
the guest stays up, then the next use lies down. `status` reports whether
the world view draws the player's own body (`own_body`): yes in bed, in
first person too, and no once up again in first person.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A sleeping villager is moved toward the foot by its standing eye height
(LivingEntityRenderer's getEyeHeight(Pose.STANDING)), not by the 0.2 of
its sleeping box, so its head lies on the pillow instead of past the bed.
- The player's own body is drawn while sleeping, in third person and in
first person (LevelExtractor skips the camera entity unless it sleeps),
lying the way the bed faces.
- Using a bed a villager sleeps in wakes the villager and keeps the player
up (AbstractBedBlock.kickVillagerOutOfBed), instead of both sharing it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PackRepository.discoverAvailable puts the packs of every source into a
single TreeMap by id, so the folder's file/ packs sort among the built-in
ones rather than after them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hash estimator placed islands with the right density but different
roots, so an estimate could vanish when the real chunk arrived. Reproduce
the feature's decoration seed, step-local index and placement modifiers
instead, run the feature on a no-op world to keep the random stream in
sync, and use those exact origins for the far cone. The per-cell biome
sampling is no longer needed; the feature's biome filter decides.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
The small_end_islands biome's only feature is end_island_decorated, so
unexplored End void rendered empty beyond the refined rings. Carry the
cell biome for void direct samples and estimate the feature: rarity and
count from its placement modifiers, in-square positions, the 55..70
height range, and the vanilla end-stone cone.
Vanilla reference: EndIslandFeature (BasicFeature::EndIsland and
minecraft:end_island_decorated), already ported in horizon-world.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
lan.rs mob_meta now builds all shared flags on main: its on-fire bit uses
is_on_fire, so a fire-immune mob never reports burning. game.rs keeps main's
charged-blaze flames with is_on_fire.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A slime block or bed bounce made no vibration. Vanilla's Entity.restituteMovementAfterCollisions posts GameEvent.BOUNCE after bounceOn (Entity.java:891); the bounce motion event was dropped.
Riding a vehicle and getting off it made no vibrations, so sculk sensors and wardens never heard it. Vanilla's startRiding and removeVehicle post entity_mount and entity_dismount at the vehicle (Entity.java:2533, 2573); derived here from the vehicle a rider had last tick.
/execute had no limit on how many sources its modifiers produce. Vanilla's BuildContexts ends the chain with command.forkLimit when the sources would reach the max_command_forks rule (silently after a forking modifier).
The wither spawn roar was a local sound at the wither, and none of the three global events checked the rule. Vanilla sends them as global level events (heard everywhere) only with global_sound_events on; without it they are plain level events the client has no sound for (ServerLevel.globalLevelEvent).
Arrows, snowballs and the other impact projectiles never broke either block. Vanilla's onProjectileHit destroys them (a pot is cracked first) when projectiles_can_break_blocks is on and the shooter may break blocks (Projectile.mayBreak).
ThrownTrident.playerTouch hands the touch to AbstractArrow only when the
player owns the trident or the owner is gone (getOwner() == null). Without
this check any guest standing on another player's landed or returning
trident took it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FoxHeldItemLayer puts the main hand item at the head's position, rolled and turned by the render state, in the ground display context; Horizon drew nothing.
main sends a wolf armor's damage and components in the body slot; the
worn armour's stacks go beside them, and the body slot never takes the
helmet's stack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CarriedBlockLayer submits the block's model on the unit cube. Horizon drew a cube of one face sprite, centred half a block off, so grass lost its sides and flowers became cubes.
Chunks, entities, POI and .dat files were capped at 1M nodes, so a chunk with thousands of full containers would not load. Vanilla reads them with NbtAccounter.unlimitedHeap().
ServerPlayer.storeGameTypes writes the previous game mode (the switcher's return mode) next to playerGameType; Horizon never wrote it, so a fresh save dropped it.
Horizon wrote empty banners and frames lists, so a vanilla map lost its markers on the first save. They are kept as read; a locked copy keeps the banner markers (MapItemSavedData.locked).
Undead burned whenever sky darkness was below 4, including the End and storms with a bright sky. Vanilla's Mob.isSunBurnTick requires the gameplay/monsters_burn attribute: overworld only, true from tick 23460 to 12542 (day.json).
They always used plain destruction. Vanilla's ExplosionInteraction.TNT picks decay or plain destruction from tnt_explosion_drop_decay, and BLOCK (End crystals, the dragon respawn spikes) from block_explosion_drop_decay (ServerLevel.getDestroyType, EndCrystal.java:102, DragonRespawnStage.java:64).
#2706 lands first; with #2927's surface column cache the check reads the
cached state instead of decoding the section.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player file that failed to load was written over with no copy. Vanilla copies it to <uuid>_corrupted_<time>.dat and loads <uuid>.dat_old (PlayerDataStorage.load).
An unreadable level.dat was treated as a new world and overwritten. Vanilla reads level.dat_old instead and moves the bad file to level.dat_corrupted_<time> (LevelStorageSource.restoreLevelDataFromOld).
Horizon wrote data/minecraft/world_border.dat at the world root, which vanilla ignores. Vanilla's ServerLevel keeps it in each dimension's data folder; the old root file is still read as a fallback.
BookViewScreen.handleClickEvent: a run_command click closes the book
(closeContainerOnServer, then clickCommandAction with no screen after it)
and runs the command; copy_to_clipboard copies and leaves the book open;
suggest_command does nothing, as BookViewScreen has no insertText. The
page's words show their show_text hover event as a tooltip
(HoveredTextEffects.TOOLTIP_AND_CURSOR), as the chat does.
Dialog.DIRECT_CODEC rejects the whole dialog for an unreadable body or input, a single option with no or two initial entries, or a text default longer than its limit. Horizon dropped the bad part and showed the rest.
The dialog screen skipped number range and single option inputs, item bodies and the entries of a dialog_list, so a server menu showed only its Back button. Draw them as DialogScreen does: a slider and a cycle button, the item widget with its description, and one button per listed dialog (external title, opens it). The client now keeps the server's dialog tags to resolve a #tag list.
DialogScreen hands an open_url to Screen.clickUrlAction (Web Links, Prompt on Links, ConfirmLinkScreen over the screen the dialog leaves). A suggest_command has no screen to take text; change_page is logged as an error.
FallbackResourceManager ignores a .mcmeta from a pack below the one that supplies the file. Horizon used the top .mcmeta of any pack, so an animation could run on a pack's static PNG.
There is no multiplayer.disconnect.invalid_name key. Vanilla's validState throws and Connection.exceptionCaught sends disconnect.genericReason with the exception.
Operator Items Tab, Quit Shortcuts and Control-click are stored and saved
like vanilla's, but no code reads them yet: grey them out like the other
placeholders instead of offering a switch that does nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main pushes mobs with the fluid current since #2554 (kacpherek, in
update_fluids, with Entity.isPushedByFluid: fish, axolotls, turtles,
frogs and swimming drowned stay put); with both, a mob was pushed twice and
the water creatures were pushed too. The flow-push LAN check stays: it
covers main's push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Function lines parsed at whatever level the caller had, so a tick or load function did nothing in a world without cheats and an owner's function could run /stop. Vanilla compiles every function line at the library's permission (function-permission-level, 2 elsewhere).
max-tick-time was read and never used. Vanilla's ServerWatchdog (DedicatedServer.initServer, when the value is above 0) logs and writes a crash report once a tick is that far past its due time, then exits with status 1.
Horizon paused the moment the window lost focus. Vanilla's pauseIfInactive waits until focus has been missing for more than 500 ms, so a brief focus steal does not pause.
main pushes mobs in a bubble column since #2555 (kacpherek, Entity's
applyEffectsFromBlocks after the move); with both, a mob got the push
twice. The bubble-column LAN check stays: it covers main's push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's dark disc and void fog read WorldGen.flat, which is now the preset
(seen by sirrayi's review).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client only had an arm for text displays. DisplayRenderer draws a block display's model or an item display's item under the same billboard and transformation, with the brightness override or the light at the entity.
Summoned displays reached clients as bare entities, so a text display showed nothing. Vanilla syncs Display's entries 8 to 22 and each type's own (text 23 to 27, block state 23, item and context 23 and 24) from the saved compound.
anger_end_time and angry_at are NeutralMob's: here the wolf, iron golem,
polar bear, enderman and zombified piglin. The bee keeps its own anger and
a piglin's is a brain memory. Every other mob wrote anger_end_time: -1,
which vanilla never writes for them, and a bee's value from a vanilla save
was replaced by it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The spawner existed only on the server, so a client never saw the item grow and turn. Vanilla tracks it with DATA_ITEM; the client draws OminousItemSpawnerRenderer and runs tickClient's particles.
A server sent every arrow as a plain one. Vanilla sends a spectral arrow as its own type and a tipped arrow's ID_EFFECT_COLOR, and the client draws arrow_spectral.png and sheds the effect swirls (Arrow.tick) and sparkles (SpectralArrow.tick).
The server waited 30 s for the answer. Vanilla disconnects with a timeout when the last keep-alive is still pending 15 s after it was sent; 30 s is only the read timeout.
finalizeSpawn's randomizeAttributes, setOffspringAttributes and a loaded
save wrote the rolled max health (and a horse's speed) straight into the
mob's stats instead of the attribute bases, so the server never sent them
and a rider's client drew the type's 53 health: 26 hearts. They are bases
now (AttributeInstance.setBaseValue), synced like vanilla's.
Fixes#2869
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_tridents quit when there was no local player, so a server never moved, hit with, stuck or returned a trident. It now finds the owner and pickers among the level's players (guests), returns Loyalty tridents to the thrower and lets guests pick them up; the spawn packet carries the thrower's id, as Projectile.getAddEntityPacket does.
Vanilla's ItemStack.applyDamage fires ITEM_DURABILITY_CHANGED for the owning player, with the stack as it was before the damage. Tools, weapons, armour, elytra, shield, mace, brush and the warped fungus on a stick now fire it, which lets ride_strider complete. The durability and delta ranges are not read; no advancement uses them.
Vanilla's ServerPlayer.onEffectAdded, onEffectUpdated and onEffectsRemoved fire EFFECTS_CHANGED; the axolotl is the source of the Regeneration it gives (Axolotl.applySupportingEffects). This lets all_effects, all_potions and kill_axolotl_target complete. Changes are seen once per tick, so one undone within the tick does not fire.
Vanilla's ServerPlayer keeps enteredNetherPosition when the player goes into the Nether and triggerDimensionChangeTriggers fires NETHER_TRAVEL with it on the way back to the Overworld. This lets fast_travel complete.
Vanilla's ServerPlayer.trackEnteredOrExitedLavaOnVehicle fires RIDE_ENTITY_IN_LAVA each tick the vehicle is in lava, measured from where the ride entered it. This lets ride_strider_in_overworld_lava complete.
Vanilla's ServerPlayer.onExplosionHit remembers the blast and trackStartFallingPosition fires FALL_AFTER_EXPLOSION when the player then falls. This lets who_needs_rockets complete.
Vanilla's ServerPlayer.resetFallDistance fires FALL_FROM_HEIGHT with the position the fall started from (trackStartFallingPosition). This lets fall_from_world_height complete.
Vanilla's ServerPlayer.triggerRecipeCrafted passes the recipe and the stacks that went in; ResultSlot, StonecutterMenu and SmithingMenu award it. This lets trim_with_any_armor_pattern and craft_decorated_pot_using_only_sherds complete, and smithing and stonecutter results now unlock their recipe too.
Vanilla's RandomizableContainer.unpackLootTable fires it for chests, trapped chests and chest minecarts, and BrushableBlockEntity does for the first brush of suspicious sand or gravel. This lets loot_bastion and salvage_sherd complete; opening marks the roll unpacked so it fires once.
Vanilla fires SUMMONED_ENTITY in CarvedPumpkinBlock.spawnGolemInWorld for players within 5 blocks of the new golem, and in EnderDragonFight.setRespawnStage(END) for the new dragon's fight players. This lets summon_iron_golem and respawn_dragon complete.
The option was greyed out and always 100 percent. The slider now moves and saves screenEffectScale, which vanilla's Options.screenEffectScale scales camera shakes and the nausea effects by.
Vanilla's SoundOptionsScreen has both next to Show Subtitles and Music Frequency. Nothing in Horizon's audio code reads them yet, so they are greyed out. Audio Device is left out: Horizon opens the default output and cannot switch it.
Vanilla's OnlineOptionsScreen has Allow Server Listing, which ClientInformation carries to servers, and the notification and presence options. Horizon has no friends list or Realms, so those three rows are greyed out.
The seven skin layers and Main Hand were saved but had no page. Vanilla's SkinCustomizationScreen lists them; the layers hide on the player model and reach servers in ClientInformation (Options.createInformation), and other players' layers follow Avatar.DATA_PLAYER_MODE_CUSTOMISATION.
Shift + use on a tame nautilus returned without opening anything. Vanilla AbstractNautilus.mobInteract opens NautilusInventoryScreen (saddle and body armour, no chest), and E while riding it opens the same screen. The mount menu now covers nautilus with its slot layout, texture, armour sprite and saddle sound.
Horizon accepted every server's Code of Conduct without showing it. The text now opens on a screen with Acknowledge and Disconnect, and the server waits for the answer. The accepted text's hash is kept per server in servers.dat, as vanilla does (acceptedCodeOfConduct, String.hashCode).
Unknown loot conditions fail and unknown number providers give 0 in both loot engines. The block engine used to pass conditions and give 1. location_check stays true there, as it has no world access at the offset.
/datapack create <id> <description> fell into the disable branch and replied with an empty unknown-pack key. It now writes <world>/datapacks/<id>/ with a data folder and a pack.mcmeta, and replies with vanilla's success and failure keys (DataPackCommand.createPack).
Trial chamber equipment tables put a copper trim on chainmail and iron armour with set_components (SetComponentsFunction), which Horizon ignored. The trim now stays on the mob's worn armour, is saved with the mob and goes onto its drops.
/damage fell back to the Generic kind for cramming, falling_anvil, falling_block, fireworks, generic_kill, lightning_bolt, out_of_world, outside_border, sonic_boom, spit, stalagmite, starve, sulfur_cube_hot, wind_charge and wither. Each now has its own kind and follows the vanilla damage type tags. Creaking, Warden and Wither ask "bypasses invulnerability" instead of "is Generic".
New chunks always used minecraft:normal, so saved amplified, large-biome and single-biome worlds got normal terrain. The preset is now read from the world's overworld generator (noise settings and biome source), and far decoration uses it too.
/list printed a fixed "1 of a max of 8" with only the host. It now lists the host of a single-player or LAN world first, then the placed guests in join order, with the real count and the hub's max, as ListPlayersCommand does. /list uuids shows each name with its UUID. A single-player owner is already a guest of the in-process server and is not listed twice.
The Versions page measures each installed version's folder on a background thread and shows the size on its row and the total under the title, so players can see where 20 GB went.
The window runs the same cleanup as the command line once an install or update is saved, and Settings gets a 'Delete old versions automatically' switch (on by default), translated to French and Russian.
Opening a world saved before DataVersion 4899 upgraded it in place with no prompt. Vanilla asks first (BackupStatus.FILE_FIXING_REQUIRED): Create Backup and Load, I Know What I'm Doing!, or Cancel. The backup reuses Make Backup, then the world opens, as EditWorldScreen does.
Horizon opened the server list at once and never read skipMultiplayerWarning. Vanilla TitleScreen shows SafetyScreen first, with a do-not-show-again box saved to options.txt, unless that option is set.
Each version is about 790 MB and an update adds one. After an install the launcher now deletes every release version except the newest two, the selected one, the ones instances pin and any the game runs from. Turn it off with prune-old-versions, change the count with keep-versions.
Fold upstream's player sweep (Sweep, sweep_players) into mace::sweep_damage;
keep melee_damage in the dragon attack; keep enchanted_damage next to
enchant_damage_bonus.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
minecraft:block_entity_data stayed raw bytes after the item sync, so a command block or spawner from a server had no data for the op warning or a spawner's tooltip. It is read as TypedEntityData (the block entity type, then its data) and given the id the saved component carries.
minecraft:container stayed raw bytes after the item sync, so a shulker box from a server showed no items in its tooltip. It is now read as ItemContainerContents (an optional template per slot), the way the saved container is.
The creative slot packet rebuilt every stack from the few components Horizon models, so moving an item with a container, unbreakable, lore or a kept raw component sent the server a stripped copy. A stack read from the wire now keeps its patch, and it goes back as it came while nothing but the count has changed.
SetCreativeModeSlot uses ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC, which puts each component value behind its byte length. Horizon wrote the plain codec, so a vanilla server read the first byte of every component as a length and kicked the player with a DecoderException.
VanillaEntityAnimOracle.batHistory set Bat.flyAnimationState and
restAnimationState through getField(..).set without setAccessible, which
Java refuses for a final field on every JDK: the bat case failed with
IllegalAccessException and took the harness down with it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fill no longer calls compute_substance for positive density or for air
above the aquifer's skip height. Both old paths cleared the aquifer's pending
fluid update; reproduce that side effect so the terrain-fill probe's
should_schedule_fluid_update matches vanilla (it caught the difference).
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
Move-only step B8 of the horizon-client crate split. The spawner, trial
spawner and beehive data live in chunk::block_entity since the
BlockEntity cut; their behaviour needs mobs, so it stays above. Once the
data is in its own crate, Rust allows no inherent impl outside it, so
each impl block becomes an extension trait in the same module, with the
same method bodies:
- beehive: OccupantExt, BeehiveExt
- mobs::spawner: SpawnDataExt, SpawnerExt
- mobs::trial_spawner: TrialConfigExt, TrialSpawnerExt
The Default impls of Spawner, TrialConfig and TrialSpawner (plain field
values) move next to their structs, since a foreign trait impl must sit
with its type. Callers add one `use` for the trait.
No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A positive-density run is exactly the default block, so the fill no longer
calls set_block_raw and the settled heightmap checks per block. One palette
lookup per section segment writes the run, its non-air count follows the air
blocks it replaced, and the heightmap is updated at the run's top only, as
rustgen's fill_initial_solid_cell does for uniform cells.
Noise 1.74 -> 1.57 ms/chunk one thread and 2.27 -> 2.10 at 13; terrain +4.4%
(min) and +1.2% (min) at 13. Block snapshots identical.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
GameRenderer.tryTakeScreenshotIfNeeded saves a world icon the Select
World list never had: while a local world is on screen without one, the
frame is grabbed after the level and the hand (before the HUD), centre
cropped to a square, averaged down to 64x64 and written to the save's
icon.png once a second until it sticks. The list already drew the icon
when a save had one; nothing ever created it.
Co-Authored-By: ZCode <[email protected]>
The surface walk read every column from paletted sections at least twice (the
top-down pass and the stone-depth look-behind). Column now keeps the resolved
ids for the read range, refilled into one buffer per chunk and updated by
set_block, as rustgen's SurfaceSystem does with read_column.
Surface 1.46 -> 1.41 ms/chunk one thread, 4.19 -> 3.89 at 13; terrain +1.4%
(min 2.6%) and +3.7% at 13. Block snapshots identical.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
Every restored dirt block under a carved cell built a fresh
MaterialDensitySources and MaterialConditions, compiled the whole surface
rule and allocated its bindings. A carver now builds one worker per pass and
moves only the 1x1x1 bindings to the cell, as rustgen's CarvingContext does.
Carvers 0.44 -> 0.40 ms/chunk on one thread and 0.62 -> 0.49 at 13 threads;
terrain +0.6% one thread and +0.98% (median) at 13. Block snapshots identical.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
The fill called compute_substance for every block although the aquifer only
does work for blocks below its skip height. Positive density always yields the
default block, and above both the skip height and the fluid level a
non-positive density is air, so the fill now decides both cases itself.
One-thread terrain 4.30 -> 4.08 s (noise 1.95 -> 1.75 ms/chunk); 13-thread
noise 2.51 -> 2.27 ms/chunk. Block snapshots are identical.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
Distant End terrain now estimates chorus plants from the biome's own
chorus_plant feature: the stem height and the age-5 tip follow the real
feature, while its placement modifiers give the density. Real chorus also
survives the refined, visited and saved far-column summaries, which
previously dropped every non-solid block.
Vanilla reference: ChorusPlantFeature and ChorusFlowerBlock.generatePlant,
already ported in horizon-world.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
The log already names the adapter and the surface format; a report of HDR
trouble now also shows whether the surface offers HDR10 or scRGB, and the
output, swapchain format and nits it turned on with.
Part of #1441.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu-hal's GLES backend takes its EGL display from the display handle the
wgpu instance was created with. Horizon created every instance without one
(`new_without_display_handle`), so on Linux EGL fell back to
`EGL_MESA_platform_surfaceless`: those configs cannot make a window surface,
the GL adapter reported every surface as incompatible (`gl not compatible with
provided surface`), and Auto never reached its OpenGL fallback.
Give the instance the window's X11 or Wayland display handle, and offer the
OpenGL choice in Options > Graphics on Linux (DirectX 12 stays Windows-only).
Tested on Intel HD 4000 with Mesa crocus (OpenGL 4.2), on X11 and Wayland:
graphicsApi:opengl, HORIZON_GRAPHICS_API=opengl and WGPU_BACKEND=gl each run
and present, and Auto falls back to OpenGL when Vulkan has no ICD.
A `/forceload`ed chunk holds a FORCED ticket at level 31, and vanilla's
level rises one per ring, so the chunk itself and the two rings around it
are level chunks with their light, the third ring has its features and the
fourth only its terrain. Horizon took the forced chunk's reach from the
caller's margin instead, so only the chunk itself ever finished and its
neighbours stayed proto chunks.
The reach now follows the ticket levels: the full ring is two chunks out,
the features ring three and the terrain ring four, and the protos of the
terrain ring are kept so the features ring can be decorated from them. A
decorated proto chunk is saved as `minecraft:initialize_light`, the status
vanilla gives a chunk that has its features but no light.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Drops the player-victim sweep in favour of #2659 and uses the
enchantment's sweeping_damage_ratio and upstream's particle queue.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The worn armour was always tinted with the colour leather has when it is
undyed, so a dyed leather piece showed the undyed brown; the trim of
another player's armour was not drawn at all, because only the item id
of the worn pieces was kept.
The vanilla's EquipmentLayerRenderer tints a dyeable layer with the
stack's dye colour (DataComponents.DYED_COLOR) and the colour it has
when undyed otherwise, and draws the trim as a layer over the armour.
The worn pieces keep their stack now, so the dye colour and the trim
reach the renderer (#2774).
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The "Thanks to ..." line read as part of the section before it (often Known issues), on GitHub and on playhorizon.dev/releases. It now has its own heading, and a horizontal rule sets the disclaimer apart. Fixes#2314.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The legacy structure pieces asked "is the collision shape a full block"
for the blocks around them, when they choose a chest's facing
(StructurePiece.reorient) and when the mineshaft checks its floor. That
is a different set of blocks than vanilla's BlockState.isSolidRender:
glass, leaves, ice, a spawner and a barrier are full collision blocks
but do not draw as solid, and soul sand and mud draw as solid without
a full collision shape.
The pieces now ask the blocks' own facts, the flag the worldgen already
keeps for every state (BlockProperty::SolidRender, which the exporter
writes from state.isSolidRender()), the way the monster room's own
reorient does. The chest's facing is decided in one function that takes
the blocks around the chest, so the rule can be checked against
vanilla's.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Vanilla keeps a neutral mob's anger in the entity tag: `NeutralMob.
addPersistentAngerSaveData` writes the `anger_end_time`, the absolute
game time the anger ends at, and the `angry_at` the target, and
`readPersistentAngerSaveData` reads them back, so a mob that is unloaded
and loaded again is still after whoever hurt it.
Horizon kept the anger as a countdown in memory only, so it was lost
with the chunk. The save now writes the anger end time and the target
with the vanilla's keys, and the load gives the mob back the anger that
is left of it and its target.
Fixes#2784
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The main hit takes getEnchantedDamage on the real attack damage, a dragon
part is never a critical hit, and the enchanted-hit burst is shown when the
enchantments add damage. Vehicles are hurt before the sweep and sounds.
Frames, paintings and cushions end the attack in skipAttackInteraction, so
they no longer sweep. Swept armor stands are knocked back.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A pack index's file name (the last /-part of an mrpack path, a CurseForge or modlist jar name) was joined onto jars/ unchecked: on Windows ..\..\x.dll or C:\x escaped it. Jar entries were checked for .. only between /. Refuse names with \, : or that are . or ..
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`DamageSource.getLocalizedDeathMessage` uses the `<id>.item` message when
the cause holds an item with a custom name, with the item's name as a
third argument: "Steve was shot by Zombie using My Sword". Horizon always
used the two-argument message, so the item was never named.
The mob's main hand item now keeps its custom name, and the death message
picks the message and its arguments from it.
Fixes#2802
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
rate-limit was parsed but never used. Average each player's packets a second as Connection.tickSecond does and kick past the limit with disconnect.exceeded_packet_rate, as RateKickingConnection does.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Deeply nested [[[... in a command, function, command block or old world overflowed the stack and aborted the process; both SNBT parsers now stop at 512 levels. Bare words such as nan, infd or Infinity were parsed as floats by Rust's parser (and became JSON null in text components); only digits make a number.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The traveller was dropped from the queue before the exit was resolved, so a
failure to resolve it lost the entity for good. The traveller now waits and
the failure is retried, with the reason in the log.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
- start a sweep (and the attack sounds, with a living target's critical
hit) when a punch lands on an armor stand, an item frame or a dragon part
- spare the entity the attack hit, and swing the arm for the stands and
frames, which did not
- hurt the other players in the sweep box through the pvp rule and teams,
for the host and for guests, with the 0.4 knockback; queued like critical
hits until the hub is back in place
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The script plays the End in a scratch home: it places a gateway beside the
spawn platform, puts a cow and a seven-diamond stack in it, and checks that
both leave it and land at the gateway's exit with their data, in the
server-thread and the in-frame singleplayer.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
A player entity keeps the profile it was created with
(RemotePlayer.getGameProfile), whose name is its scoreboard name, even
once ClientboundPlayerInfoRemovePacket drops it from the player list.
Servers rely on that to hide their NPCs (fake players) from the list
after spawning them on a team, as Hypixel SkyBlock does with
collisionRule never. Horizon read the name from the list, so an NPC
removed from it lost its team and pushed the local player again.
Remote::level_player_names keeps the name each player entity spawned
with; Game::remote_player_name and remote_player_names (teams, name tags,
glowing) read it, falling back to the list entry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shield's use animation is the BLOCK one: AvatarRenderer.getArmPose
raises the arm for it and Item.getUseDuration gives it 72000 ticks, so
the arm stays up while it is used. The fix (kacpherek's #2310) sets the
blocking flag for the other players and gives the shield its use
duration; these tests pin both ends: the arm goes across the body while
the shield is used, and the shield's use duration is vanilla's 72000
ticks, not the 0 an item without an use animation gets (#2289).
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Entity.getTeam looks a team member up by getScoreboardName, which is a
player's profile name (Player.getScoreboardName) and any other entity's
UUID. Game::mob_team, mob_teams and player_allies used the UUID for every
entity a server sent, players included, so another player's team was
never found. On a server that turns collisions off with a collisionRule
never team (Hypixel SkyBlock), the other players still pushed the local
player; the same lookup feeds allies, friendly fire, glowing colours and
the team checks explosions make.
Game::scoreboard_name now picks the profile name for another player (from
the player list, via Game::remote_player_name) and the UUID otherwise.
lan_e2e gains a team-push check: the host pushes the guest without a
team, and doesn't once the host's team has collisionRule never.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- play the sweep sound before the victims are hurt, then draw the arc
- hurt armor stands (not marker ones) in the sweep box through the same
hit a direct punch gives them
- run the victims' damage through getEnchantedDamage with the real base
- sweep around a boat or a minecart that takes the hit, which only played
the sound before
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Player.doSweepAttack was only a particle and a sound that never fired: the
player's speed field is refreshed by the local movement step only, so it was
0 for the player a server simulates and isSweepAttack could not pass.
- read the movement speed attribute when checking for a sweep
- hurt the other mobs in the target's box inflated by (1, 0.25, 1) and under
3 blocks away for 1 + sweeping ratio * damage, scaled by the charge, with
0.4 knockback and the post-attack effects when the hurt lands
- send the arc as a LevelParticles packet so a server's clients draw it
- take its position from Mth's sine table
Fixes#2764
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds Simplified Chinese (zh) beside English, French and Russian: the 160 strings
of launcher/lang, written for Horizon, and the language in Settings > Launcher,
the system pick (zh-Hans-CN and other zh locales) and `settings language zh`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Upstream now keeps a gateway's exit in the gateway's own block entity
(TheEndGatewayBlockEntity.setExitPosition), so the player travels to the
exit the gateway was given (#1517). The entity travel stays: the mobs and
the dropped items go through the same gateway, and the exit the first
traveller found is reused, so the player and the entities land in the same
place and only one island is built for a gateway.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The pull request's last push did not start a run; this empty commit
starts one on the same tree.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
main's Superflat Customize screen (#2214) still set the removed WorldOptions.flat: it now picks WorldPreset::Flat,
and the agent's customize-flat state reads the preset. structure_mobs.rs's test passes the new preset argument
(minecraft:normal) to VanillaTerrain::for_dimension.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The braces were one too many, so the file did not parse and the Python
tests failed on it. The command it builds is the same now: the item
stack {Item:{id:"minecraft:diamond",count:3}}.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Only the player was checked for End gateways, so a cow or a dropped
stack standing in one stayed there for good. The entities now go
through a gateway the way the player does: the block their box swept
through takes them in (unless the gateway is cooling down, as
EndGatewayBlock.entityInside does), they travel on the next tick
(Entity.handlePortal, with the End gateway's transition time of 0) and
land where the gateway leads once its chunks have loaded, keeping their
data and counts.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.
The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Brings main in so the branch has one merge base with it (GitHub saw a
conflict through the criss-cross bases; the merge itself is clean).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The CI's rustfmt (stable) writes these signatures on one line; the local
toolchain's rustfmt split them, so the check job failed on the formatting
alone.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
A mob standing in flowing water was not carried along by it: the fluid machinery that computes a current (horizon-world's entity_fluid, the port of vanilla's EntityFluidInteraction) was wired to the player, the dropped items, the arrows and the primed TNT, but the mobs use a reduced fluid model of their own and never felt a current.
The mobs now run the same per-tick fluid update as the other bodies (Entity.updateFluidInteraction): the flow of the fluid they stand in is accumulated over the blocks of their box and added to their velocity, with the water scale 0.014 and the lava scale 0.007 (0.00233 without the fast lava dimension attribute), the current of a player being the average of the flow vectors and of any other body its direction.
The new flow-push LAN case puts a mob in a flowing water block and a second one in a pool of still sources, and checks that only the mob in the current is carried along.
The branch also drops the unreachable second "minecraft:rabbit" arm in crates/horizon-client/src/entity_nbt.rs, which clippy rejects upstream.
Fixes#2549
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The merge of #2585 left two "minecraft:rabbit" arms in the same match, so
the second one is unreachable and clippy's -D warnings rejects the build
(upstream's own checks job is red on it since 0e793be2a). The first arm
already writes RabbitType and MoreCarrotTicks, so the second one is dead
code; its note about the legacy id moves to the arm that runs.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
A mob standing in a bubble column was not moved by it: the block's
`entityInside` had no counterpart on the entity side, so only the local
player (whose motion code has it) and the arrows were affected. The mobs
now get the same push vanilla gives them in `BubbleColumnBlock.entityInside`
-> `Entity.onAboveBubbleColumn` / `Entity.onInsideBubbleColumn`: the body's
velocity along the vertical axis is raised by 0.1 (0.06 when the block above
the column is solid) and capped at 1.8 (0.7), or lowered by 0.03 and floored
at -0.9 (-0.3) for a column that drags down, and the fall distance is reset
inside a column.
The new `bubble-column` LAN case puts a mob in a soul sand column and a
second one in plain water, and checks that only the mob in the column is
carried up.
Fixes#2546
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
While the HDR setting is on, F3 shows the swapchain HDR presents to, with
the paper white and peak ("HDR: HDR10 (PQ), 200/500 nits"), or that the
display offers none. With the setting off nothing is added, so the screen
stays as vanilla's.
Part of #1441.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Light level says how far a source lights its surroundings, not how bright
it looks: a candle's flame is as bright as a torch's. Light sources now
get a tier (mesher::hdr_tier): very bright (the sun), bright (glowstone,
campfires, the soul fires), incandescent (flames, lava), low (redstone
torch, magma, a candle's wick) or none (glow lichen). Unlisted ones go by
their level. The tier rides in the emission bits as the level did, and
each has its own share of the headroom and its own lift window
(gfx/hdr.rs TIERS), tuned by eye on an HDR1000 OLED.
Flames, lava drops and obsidian tears now shine too: their tier rides in
the particle vertex's overlay bits, and with HDR on a second draw of the
entities writes only the frame's alpha for them, so their colour still
blends as before.
HORIZON_HDR_DEBUG=1 now greys what isn't lifted and tints what is, so a
lift shows on purple and red light too.
Part of #1441.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horse, AbstractChestedHorse (donkeys, mules, llamas) and ZombieHorse.mobInteract:
with something other than food in hand, an untamed one is made mad (makeMad:
standIfPossible and the angry sound) and the player is not seated; an untamed
skeleton horse ignores the click (SkeletonHorse.mobInteract). The shared
equine::held_item decides it for the game and the mob_ai probe.
makeMad goes through makeSound (0.8 volume, the voice pitch from the mob's
random), with the baby horse's and the zombie horse's own angry sounds.
Donkeys and mules rear too (canPerformRearing is false only for llamas), a
rearing or eating horse runs no goals (AbstractHorse.isImmobile), and
RandomStandGoal plays the ambient sound, as vanilla's getAmbientStandSound.
On a server thread, a horse that bucks (RunAroundLikeCrazyGoal) now throws
off the server player riding it, not only the window's own player.
mob_ai harness: `interact` uses an item on a mob (Mob.mobInteract), with
sword scenarios for horses, donkeys, mules, zombie and skeleton horses and
llamas, foals included.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Most of the Nether's surface rule ends in netherrack, written over the
netherrack the noise step placed: every solid block paid for a section
write, two state table reads, four heightmap updates and a fluid check
for nothing. Column::set_block now leaves a block that already holds the
state (a fluid is still marked for post-processing, as every write marks
it).
chunkgen_bench, 576 chunks, 1 thread: Nether surface 1.81 -> 1.52 ms per
chunk; the terrain snapshots of seeds 12345 and 1 (Nether and Overworld)
are byte for byte the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Join a friend, the mods search and the Servers fields (create, players,
console, MOTD, port, token, email) had their own String and their own
key handling, so none of them could move the caret, select or undo.
They use LineEdit now, and type_key and clip, which only they used, go.
description:Fix a Horizon bug or gap so it behaves 1:1 like vanilla 26.3, starting from the port ledger (tools/port_ledger.py) instead of grepping. Use for any issue, Discord report or PR that makes Horizon match vanilla:"X doesn't work like in Java",a missing sound, particle, animation, mob behaviour, block rule, screen or packet.
---
# Vanilla parity with the port ledger
The port ledger pairs every vanilla method with the Rust fn that ports it (or says it is missing, or needs
no port) and lists where a port isn't wired like vanilla. Ask it first; grep only for what it can't answer.
Full reference: `docs/port-ledger.md`.
## 0. Have a fresh ledger
```sh
python3 tools/port_ledger.py build # ~20 s; again after pulling main
```
It needs the vanilla jar (the launcher's `versions/26.3/26.3.jar`, or `--jar`) and/or the lab's decompiled
sources (`python3 tools/lab.py decompile`, or `--sources DIR`). If `build` says it has no vanilla code, run
the lab first; don't guess vanilla from memory.
## 1. From the report to the code
```sh
python3 tools/port_ledger.py issue 3236# a GitHub issue number
python3 tools/port_ledger.py issue "mooshroom stays red after lightning"# or the report's text
```
It ranks the vanilla classes the report names and lists, per class, its Rust home files, where each method is
ported, and the known gaps (`missing` rows from the verdict map, seam/client/drift gaps). Often the cause is
already written there ("Type is not saved"). Then drill in:
```sh
python3 tools/port_ledger.py where MushroomCow.readAdditionalSaveData # ports, callers, callees, gaps
python3 tools/port_ledger.py class MushroomCow # every method: where, or missing
python3 tools/port_ledger.py what crates/horizon-client/src/mobs/farm.rs:120 # which vanilla method a fn ports
```
A ledger answer is a lead, not a verdict: about 1 in 12 map rows is wrong. Read the vanilla method (path and
line are in the output) and the Rust fn before changing anything.
## 2. Port it the vanilla way
- Translate the vanilla method, don't reinvent it: same order of operations, same constants, the same random
draws in the same order (`horizon_core::random`), Java `Math` through `horizon_core::jmath`.
- Open the fn's doc with the vanilla method it ports: `` /// `MushroomCow.readAdditionalSaveData`: ... ``.
That one line is what makes the next agent find it.
- Fix the path players run: single player is a client of the integrated server, so server logic lives in
`game/lan*.rs` and `src/lan/`, and what only the client's copy shows (particles, local sounds, hand
animation) must be reached from `game/remote*.rs`. A fix on the old in-frame path alone fixes nothing.
- Look at the neighbours the ledger lists for the same class: a `client` or `seam` gap next to your fix is
<!-- Opened by an AI agent? First line: "> 🤖 Opened by <your GitHub login>'s AI agent (<tool, model>), not by <your GitHub login> personally." Use your own login, not someone else's. -->
## What and why
<!-- What this changes, and why. Link the issue it fixes ("Fixes #123"). -->
## Kind
<!-- Tick one: it sets the pull request's label (CONTRIBUTING.md "Pull requests"). -->
- [ ] parity: Horizon now does what vanilla 26.3 does
- [ ] bug: a crash or a breakage
- [ ] perf: faster, or less memory
- [ ] feature: something new for players (listed under New in the release notes)
- [ ] extra: Horizon-only, off by default
- [ ] tooling: tools, CI, tests, harnesses
- [ ] docs
## Vanilla reference
<!-- The vanilla classes and methods this follows (tools/rea_vanilla.py), and the harness that checks it.
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
## Crates
@@ -35,11 +39,12 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~170 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-client` | The game: a lib (`src/lib.rs`, ~100 flat modules: gameplay, networking, title screens) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
@@ -110,7 +118,8 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`L:` = lib (`crates/horizon-client/src/`, or `crates/horizon-client-render/src/` for the modules that crate holds, under
the same `horizon_client::<module>` paths), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
@@ -162,17 +171,18 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
"Server thread" with one level per dimension; the window joins it as a client through memory (`remote`),
exactly as a LAN guest or a dedicated server's player does. This is what players run. Open
`[server-thread]` issues list the gaps left.
2.**In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
the world itself, one level that follows the player (`fn enter_dimension`). Deterministic rule fixtures still use this path; player-facing integration uses the default server thread.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`): DEPRECATED, never use it, it
will be removed soon.** The window's `Game` ticks the world itself, one level that follows the player
(`fn enter_dimension`). Compatibility does not matter: don't add features or fixes for it, don't write new
tests against it, and move code off it when you touch it. The deterministic rule fixtures still run on it
until they are ported.
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
when you touch player-facing gameplay:
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test the server
thread when you touch player-facing gameplay:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by 281 differential harnesses" width="100%">
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by more than 300 differential harnesses" width="100%">
</p>
<p align="center">
@@ -23,15 +23,15 @@
# Horizon
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by more than 300 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## Download
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your copy of Minecraft (your own install, or downloaded from Mojang for your account), and updates itself.
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs the game and keeps it up to date with each new build (Horizon ships on one channel, the Nightly), prepares its files from your copy of Minecraft (your own install, or downloaded from Mojang for your account), and updates itself.
**You need** a Microsoft account that owns Minecraft: Java Edition (the launcher signs in with it, as other launchers do; see [Microsoft account](docs/microsoft-account.md)) and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Nothing else: with no Minecraft on your computer, Play downloads 26.3 and the Java it needs from Mojang for your account, as the official launcher does, then prepares it once. An install you already have (the official launcher, Prism, MultiMC and other launchers all work) is used instead. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
**You need** a Microsoft account that owns Minecraft: Java Edition (the launcher signs in with it, as other launchers do; see [Microsoft account](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 on Linux, Vulkan or DirectX 12 on Windows, or any Apple Silicon Mac. Nothing else: with no Minecraft on your computer, Play downloads 26.3 and the Java it needs from Mojang for your account, as the official launcher does, then prepares it once. An install you already have (the official launcher, Prism, MultiMC and other launchers all work) is used instead. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
@@ -46,9 +46,9 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates, item modifiers and world generation (vanilla's heights and biomes for now).
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining servers, online-mode ones with the Microsoft account the launcher signs in with. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
- **Mods:** Horizon has its own mods, installed from the launcher. The official ones (Zoom, FPS Counter, Keystrokes and Minimap) are native Rust that the launcher builds into your game, with no Rust to install; older WebAssembly mods still run while they move. Java mod jars do not run in Horizon. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
@@ -139,6 +139,11 @@ average; `tests/test_parity.py` keeps it in sync with this table. Last audited
rerun whenever one of its inputs changed, and the Mac mini now runs every one of
them too (Rust uses Java's own float maths on each platform).
The harnesses measure behaviour. A second measure is method by method: the [port ledger](tools/port_ledger.py)
lists every method of vanilla 26.3 next to the Rust functions that port it, with a verdict for each one
(ported, folded into another function, missing, or needing no port), and the untested ports. Run
`python3 tools/port_ledger.py build`, then `coverage`, with the vanilla data in `.local/`.
| Subsystem | Score | Main gaps |
| --- | ---: | --- |
| Window, input and game loop | 1 | None: the game loop, options, camera, auto-jump, toggle sprint/sneak, local input and the frame limiter match vanilla |
/// How long the trial run plays: two seconds of game.
constTICKS: u64=40;
constFRAMES_PER_TICK: u64=3;
pubstructReport{
pubkind: Kind,
pubfacts: Facts,
pubsha256: String,
pubsize: u64,
pubsigned: bool,
/// A mod has code to run (a content mod may have none).
pubcode: bool,
/// A mod's component imports.
pubimports: Vec<String>,
/// A mod's commands, each run once.
pubcommands: Vec<String>,
/// What the mod showed during the trial run.
puboutput: Vec<String>,
}
implReport{
@@ -53,10 +37,6 @@ impl Report {
"sha256": self.sha256,
"size": self.size,
"signed": self.signed,
"code": self.code,
"imports": self.imports,
"commands": self.commands,
"output": self.output,
})
}
@@ -100,18 +80,8 @@ impl Report {
ifself.kind==Kind::NativeMod{
out+=" native source: unsandboxed; not executed by the registry check. Compile and test against the exact pinned Horizon and Mixins revisions before review.\n";
description="The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
Loaded 100 of 905 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.