🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `Entity.DATA_SHARED_FLAGS_ID` bits (on fire, crouching, swimming, invisible, glowing, fall flying) and the pose for LAN players; your follow-up also sends the crouching pose under a low ceiling, which matches `Player.updatePlayerPose`. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `CreakingHeartBlock` / `CreakingHeartBlockEntity.removeProtector` on an explosion kills the bound creaking. Left for later: no XP for a heart broken by a player's TNT (Horizon doesn't track the TNT's owner yet). Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `NoteBlock.triggerEvent` adds the note particle 1.2 above the block with note / 24 as its colour, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ChunkMap.TrackedEntity.getEffectiveRange` ends in `scaledRange`, which applies `entity-broadcast-range-percentage`, as done here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `HoneyBlock` (`isSlidingDown`, `doSlideMovement`, `maybeDoSlideEffects` with its 1-in-5 sound and particle rolls, entity event 53). Your follow-up runs it for a server's player too, with the sound to the others and event 53 to everyone, which is how vanilla splits it. Left for later: mobs don't slide yet, and there is no `honey_block_slide` criterion. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `WitherRoseBlock`'s effect (wither for 40 ticks on the mobs inside it) and the eyeblossom's poison on bees. Since then you added `MobEffectInstance.update`'s rule (a stronger effect replaces, an equal one keeps the longer duration, a weaker one changes nothing), which matches. Left for later: a weaker, longer effect is not kept hidden to resume afterwards (`hiddenEffect`). Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's particle providers and `animateTick`s: honey drips (`DripParticle`), the poplar and azalea leaf particles, falling dust under gravity blocks and the campfire food smoke match vanilla's rates, sprites and velocities. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SnowGolem.DATA_PUMPKIN_ID` and `MushroomCow.DATA_TYPE` with their layers (`SnowGolemHeadLayer`, `MushroomCowMushroomLayer`), as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: launcher-features-1 checked this: base main, clean merge with main, launcher CI green; fmt, clippy -D warnings and the launcher tests pass locally. Queued for merge.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LevelEvent` 2002 / 2007 (`LevelEventHandler`: the item crumbs, then the coloured spell or instant effect particles) and the 1053 / 1054 break sounds. It conflicted with main in `game.rs` (the zombie door sounds 1019 / 1021 landed there), so I merged main in, keeping both lists. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Entity.canSpawnSprintParticle` and `spawnSprintParticle` (the block under `getOnPosLegacy`, clamped into it, fences and walls taken from the supporting block) match. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: the parity stop at c5534f0d7 was friends_integration, which main broke through #2501 and #2598 fixes, plus remote_fireworks and server_player_sculk (load on the old runner; both pass on main on the Mac mini). None of the three is from this PR. Merged main (fde2dfffe, a clean merge); pr_ready --quick passed: 1357 tests, clippy, launcher.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the server sends entity event 4 once its warmup ends (`EvokerFangs.tick`), `handleEntityEvent(4)` starts the client's attack and plays the sound locally, and the client only counts `lifeTicks` down for the animation, while the server deals the damage and discards. Moving the host's sound to `play_unshared` matches that, since each client plays its own. It conflicted with #2094 (both hook entity events) once that merged, so I merged main into it, keeping both calls. `lan_e2e.py --check fangs` passes in both modes. It conflicted again with main in `tools/lan_e2e.py` (new checks on both sides), so I merged main in once more, keeping every check; clippy is clean. Fixes#1482. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `EnderDragonFight.tick` scans the state and places the exit portal only when `dragonEvent.getPlayers()` is not empty, so a force-loaded End with nobody in it gets no portal, as here; the new `forced_end` harness compares both servers. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `WorldGenRegion.setBlock` gives a block with a block entity a `DUMMY` entry that becomes `newBlockEntity`'s default, so the deep dark's sensors, catalysts and shriekers and potent sulfur get theirs, while placed data is kept. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SpikeFeature` places the end crystals during decoration, so they are saved with the chunk that generates them, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FallingBlockEntity` with a `BrushableBlock` breaks it where it lands (`onBrokenAfterFall`'s sound and level event). Left for later: the game event uses the block position, not the bounding-box centre. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ForceLoadCommand` adds its FORCED tickets x-outer, z-inner, all at once, so the decoration order follows from that; the new `forced_order` reproduces it with the same ticket simulation as `vanilla_order`. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: a chunk's `STRUCTURE_STARTS` are its `getAllStarts`, whether or not a piece reaches it; `starts_at` now gives those, as vanilla saves them. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `StructureTemplate.placeEntities` and the pieces' markers call `finalizeSpawn(level, difficulty, STRUCTURE, null)` inside the `WorldGenRegion` with its random, and the save keys now follow `addAdditionalSaveData`. Your rebased head is the reviewed change plus the comment cleanup, on current main. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: these three options have no system in Horizon yet, so they are drawn inactive with the same notice the other unavailable options use, and clicks do nothing. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `MusicManager.MusicFrequency` and the option's listener (`setMinutesBetweenSongs`). I pushed one fix: in `SoundOptionsScreen` Music Frequency starts its own row (beside Music Toast), not beside Show Subtitles. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `EntityAnchorArgument` and `execute positioned as` (it keeps the anchor). I pushed one fix: a small armor stand's eyes are at 0.9875 (`BABY_DIMENSIONS.withEyeHeight`), not half the type's, and the types' eye heights are floats widened as `getEyeY` does. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the High Contrast option adds or removes the `high_contrast` pack (`Options.updateResourcePacks`), and your follow-up keeps the pack screen's selection in step. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `HumanoidModel.setupAttackAnimation` and `AnimationUtils.swingWeaponDown` use the main arm of a left-handed mob, and `PiglinModel`'s crossbow and admiring poses mirror with it. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
🤖 Agent: play into a world / join a server + servers.dat list (item 4 of launcher-features-1), on main after #2593: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally (servers.dat round trip keeps every field and hidden entries); screenshots in the PR.
🤖 Agent: worlds in the launcher (item 3 of launcher-features-1), on main after #2591: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally (vanilla-style backup layout, copy naming); screenshots in the PR.
🤖 Agent: version per instance (item 2 of launcher-features-1), now on main after #2590: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally (pin/unpin test); screenshots in the PR.
packs.rs lists an instance's resource packs (resourcepacks/, a .zip or a
folder with pack.mcmeta) and shader packs (shaderpacks/: Horizon's own and
the shaders/ format, as the game's Shader Packs page lists them), and turns
them on or off the game's own way: resourcePacks in options.txt (a pack
turned on goes on top) and pickedShaderPack in horizon-graphics.txt (one at
a time); every other line of both files stays. add() copies a dropped file
into the right folder under a free name; remove() turns it off and moves it
to the trash.
CLI: `packs [list|add|on|off|remove] [--kind] [--instance]`; `packs remove`
needs confirm over MCP.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: fixes parity-main's friends_integration (red since 4be5c8eb5). Cause: #2501 spawns new players at a random spot near the spawn, as vanilla does, and test_friends.py assumed the guest stood on the spawn itself. Main failed 4/4 on the Mac mini, main with #2501 reverted passed 2/2, and this branch passed 4/4 on the server thread and 1/1 in-frame. Python only; harness-only, skip-release.
🤖 Agent: launcher instances in the window (item 1 UI of launcher-features-1), now on main after #2587: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally; screenshots in the PR.
🤖 Agent: launcher instances core (item 1 of launcher-features-1). Verify and the three-platform launcher build passed; fmt, clippy -D warnings and all launcher tests pass locally, including the migration test (no existing file moves); CLI and play checked in scratch homes.
Home shows the selected instance's newest screenshots (what F2 saves in its
screenshots/ folder), all of them on request, and opens one over the window
with Newer/Older (and the arrow keys), Copy (the picture to the clipboard),
Open, Show in folder and Delete (to the trash, after a confirmation).
CLI: `screenshots [list|delete] [--instance]`; `screenshots delete` needs
confirm over MCP. worlds::to_trash is shared by worlds and screenshots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: game quick-play flags (item 4 of launcher-features-1): pr_ready --quick passed (1281 horizon-client tests, clippy, fmt); verify passed and CI's parity scope selected no harness for this argument-parsing change; checked by hand with the real game (missing world → title screen, existing world opens).
Every run of the game started from the window or `horizon-launcher play`
adds its length to its instance's play time in instances.json (by id, so a
renamed instance keeps it), with when it ended. Home's Instance row shows
"3 h 06 min in total · last played today"; `instance list` shows it too,
and its JSON gives played_secs and last_played_ms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each world on Home gets Play (the game starts with --world <folder>), and
Home lists the selected instance's servers from the game's own servers.dat:
Join starts the game with --server <address>, Add server and Remove change
the file. server_list reads and writes it as vanilla's ServerList does
(uncompressed NBT, a new file then servers.dat_old) and writes every entry
back with all its fields, the hidden Direct Connection ones too.
CLI: `multiplayer [list|add|remove] [--instance]`, `play --server`;
MCP: play takes `server`; `multiplayer remove` needs confirm.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. Formatting only: Cargo.toml's workspace members one per line (38, including #2160's), so new crates stop conflicting. tomllib parses it identical to main and Cargo.lock is unchanged, so no parity run is needed; verify and launcher pass.
The launcher starts the game straight into a world or a server. Vanilla's
names for that (client Main, QuickPlay) now work too: --quickPlayMultiplayer
is --server; --quickPlaySingleplayer opens a world of saves/ only if it
exists, as QuickPlay.joinSingleplayerWorld does, and shows the title screen
otherwise (--world also creates one).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Home lists the selected instance's worlds from their level.dat (name, mode,
version, last played) with their size and picture. Back up zips one into
backups/ as vanilla's Make Backup does; Copy makes "<name> (1)" with that
name in its level.dat; Delete moves it to the system's trash after a
confirmation (the file manager call on macOS, no Finder permission);
Open folder. `horizon-launcher worlds list|backup|copy|delete`, and their
MCP tools (delete needs confirm). level.dat is read with horizon-world's NBT.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging the first crate split (horizon-command + horizon-client-core, pure move, re-exported under the old paths). CI parity passed at 17f8419ac (head merged into main), verify and launcher pass, approved by rdbt's review agent; landed by hand while the REST pool refills.
An instance plays the launcher's version, which follows updates, unless it is
kept on another installed one: the Play page's picker pins the selected
instance (picking the version marked "follows updates" unpins it), and
`instance version <instance> <tag|latest>` does it from the CLI. play,
`play --instance` and MCP play use the pin; a pack's Play uses its instance's.
The sidebar shows a pinned instance's version, or that it is not installed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The sidebar lists the instances by group with their icons (a texture from the
player's prepared files, drawn as pixels, or a colour); clicking one selects it
for Play. + makes one, empty or a copy of the selected one (copied in the
background); Home's Instance row edits its name, group and icon, copies it, and
opens its folder. The dialog's name and group are the launcher's shared text
fields.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #2501 a player new to the world spawns at a random free spot near the
world spawn, as vanilla's PlayerSpawnFinder does. The guest often stood against
a hill, so walking forward moved it less than a block, or terrain hid the block
it had to aim at: friends_integration failed on every run on the Mac mini.
The host now clears a floor beside itself and teleports the guest there before
the movement checks, and the block check waits for the guest to stand still.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game's folder stays the default instance, "Horizon", where it is; imported
and registry modpacks are listed as instances in their own folders; new ones
go to instances/<slug>/ in the game's folder, empty (the player's options and
offline name) or a copy of another (never sessions, logs or telemetry). Names,
icons and groups live in the launcher's instances.json, so nothing moves.
Play, `play --instance` and MCP play start the selected instance, which
follows the launcher's data collection answer and No AI. Crash reports are
read from the run's own folder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A manifest line defining or turning on a workspace crate's feature no longer
makes the manifests count, if every source naming the feature (or one it turns
on) changed too, as in a split, where the new crate's files are all new.
#2215 (horizon-items with a test-support feature) now selects 209 harnesses
instead of 354.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On playhorizon (frames drawn on the CPU) a screenshot takes ~0.6 s: with a
16-tick wait the later shots came after the sparks' lifetime, and the setup's
own red chat lines, gone by then, made the host's count negative.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each new crate then adds one line instead of rewriting the whole list, so
PRs that add crates at the same time stop conflicting (the crate split's
#2160 conflicted on it three times).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Axolotl.handleAirSupply` (6000 air, refilled by water or rain, `dry_out` damage at -20) and `Squid.hurtServer`'s `spawnInk`. I pushed two fixes: `makeSound`'s voice pitch is drawn even when the squid is silent, so the ink's random draws stay in vanilla's order; and rain now uses the biome's height-adjusted temperature (`Biome.getTemperature` with the frozen modifier and the drop above sea level, `Level.isRainingAt`), not the base temperature, so an axolotl high up a mountain is not refilled by snow. On main with this head: clippy `-D warnings` clean and the 1186 horizon-client lib tests pass. CI parity runs the harnesses this head reaches. Thanks @kacpherek!
The maintainer asked that CI stop using his desktop (2026-10-26). playhorizon's
runner now carries horizon-game and the desktop's runner none of the job labels;
full-parity jobs go to playhorizon only, with a 6 h timeout on its 4 CPUs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cargo.lock is a shared input and every harness depends on its crates'
Cargo.toml, so a crate split (#2160) selected all 352 harnesses although
its moved code reaches 123. With --changed, Cargo.lock and Cargo.toml
changes that only add, remove or move workspace crates (no registry or git
package, version, feature, dependency or build setting changed across them,
compared with the merge base) are left out; the moved code counts through
its own sources. Any other manifest change still counts, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under parity.py the release and fast profiles have identical settings
(PROFILE: no LTO, 16 codegen units, incremental), yet 38 harnesses built
horizon-client in release and 16 others in fast: every run where the client
changed compiled it twice. They all use fast now. Release stays for
horizon-world's probes, which no fast harness builds.
Measured on playhorizon (4 CPUs), all 13 probe builds after 78 merges:
1578 s, of which the release client (bin + client_probe) took 485 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Structure mobs (bastion piglins, brutes and hoglins, ocean ruin drowned,
village and mansion mobs, witch huts, monuments) were tagged and finalized
at load by the cut-down spawning routine: no STRUCTURE reason (piglin
weapons rerolled), no Mob step (follow range bonus, left-handedness), no
baby hoglins, no brain memories, no drowned nautilus mounts, and draws
from each mob's own random. The decoration region now calls a hook the
game installs in the generator workers, which runs mobs::finalize with
Reason::Structure, the region random in placement order and the level's
difficulty with no inhabited time; jockeys come back as the vehicle with
its Passengers. A mob the hook cannot finalize keeps its tag and is
finalized on load as before, as are mobs of older saves; other entities
drop the tag. The generator gets the level's difficulty as soon as the
level opens, so the spawn area's mobs use it too. mob_to_nbt writes Invulnerable and LeftHanded always, IsBaby
for zombies and piglins, Age for hoglins, the finalized attributes and
memories, and drops the template keys vanilla never saves.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. Ludovic asked for the Sculk banner visible on open without scrolling, hideable after a while. Launcher run with isolated homes: banner at the sidebar's foot, HIDE after 3 min seen; clippy -D warnings and 137 launcher tests pass.
A "Hide" shows beside its label after the banner has been on screen for
3 minutes, counted over every start (settings: partner_seen_secs, saved
each minute). Hiding it is kept (partner_hidden); the Servers page keeps
its own banner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Home it sat under every row, so nobody saw it without scrolling. It now
sits small (228 px wide) above the Discord and Website links on every page
but Servers, which keeps the large one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3 Soul Speed: the speed on soul blocks, no soul sand slowdown, the particles and sound, and boot wear on 4% of ticks, now also for each player on a server (`guest_server_tick`), with the particle sent to the players around. I pushed one fix: the random draws come in vanilla's order (the 0.04 wear roll first, then the particles, then the 0.35 sound roll and its pitch). It then conflicted with main (#2190 added the elytra glide wear in the same tick), so I merged main in: Soul Speed comes first, as `LivingEntity.baseTick` runs before `aiStep`, then the glide wear, so both draw from the level random in vanilla order. On that merge: clippy `-D warnings` clean, horizon-client tests pass, the fast build links, and `lan_e2e.py` elytra, damage and breath pass with the host in-frame and on its server thread. CI parity runs the harnesses this head reaches. Thanks @kacpherek!
🤖 Agent: #2318 has merged and this PR's base is main. The official Recipes mod (needs API level 6): pr_ready --quick green; the e2e passes 25/25 in-frame and on a dedicated server on this head (61 of 63 in five modes before; the 2 failures were #2501's bug, now merged). Queued at 92e3f496f.
🤖 Agent: #2306 has merged and this PR's base is main. recipes.place and recipes.give at API level 6, give cut to one stack. Sandbox (31), api_reference and clippy -D warnings pass; the actions e2e passed 58/58 in five modes. Queued at bd1436f16.
🤖 Agent: #2305 has merged and this PR's base is main. Recipe pages with station tabs and an ellipsis on a long title; clippy -D warnings with --features mods and --no-default-features is clean, and the browser e2e passed 98/98 in five modes. Queued at 0cba2edbd.
🤖 Agent: verify passed (incl. Linux clippy for the glibc malloc_trim path); chunk_generation, worldgen, worldgen_random, save_roundtrip and world_reopen pass on this head; pr_ready --quick green. Preload starts on world select per the orchestrator's decision. Perf fix, no label.
🤖 Agent: #2301 has merged and this PR's base is main. browser.set-recipes (with station tabs) at API level 6; sandbox (31), api_reference and clippy -D warnings pass. Queued at a373eea18.
A crate split reaches every harness (#2160: 352 harnesses, 71 min on a Mac
mini runner) and held the Mac mini's runners while other pull requests'
parity jobs queued for over an hour. With the full-parity label, the job
runs on the desktop's runner only (8 at a time) and waits for it while the
desktop is in use; its timeout is 300 min, since a job under way when
someone comes back goes on with 2 threads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: #2300 has merged and this PR's base is main. Merged up the renumbered stack (browser at API level 6); clippy -D warnings with --features mods and --no-default-features is clean, and the browser e2e passed 98/98 in five modes before. Queued at a2cd70e8e.
🤖 Agent: #2284 has merged and this PR's base is main. browser is at API level 6 (recipes took 5). Sandbox (31, the UI guest at level 6), api_reference and clippy -D warnings pass. Queued at b836eea06.
🤖 Agent: #1895 has merged and this PR's base is main. Merged main: recipes moves to API level 5 (world took 4). Sandbox (30, including the level assertion), api_reference and clippy -D warnings on horizon-mods pass. Queued at b360e3c26.
🤖 Agent: LAN guests (host on its server thread) and a dedicated server's new players joined inside each other on the exact spawn, so they clicked the other player instead of a table or chest. New players now get PlayerSpawnFinder.findSpawn as in vanilla. lan_e2e spawned/crafting/chest pass in both host modes, a dedicated two-guest check passes, and the 32 affected harnesses pass.
glibc keeps the pages the preload thread freed in that thread's arena, and
macOS keeps them in its zones until memory runs short, so dropping the
preload didn't lower the footprint: horizon-threads now asks the allocator to
return free pages (malloc_trim, malloc_zone_pressure_relief).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The command ran on after remote.sh was stopped or its connection dropped,
holding the name's lock, and a game or daemon it left behind outlived the
run. It now runs in a scope named after the run (remote-<name>.scope),
stopped when the command ends, when the ssh session ends first, and before
the next run under that name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The harness sampled the fireball from a new horizon-ctl process each time,
10-16 ticks apart on a loaded runner: the fireball (~2 blocks a tick) left
the cleared corridor or the guest's tracking range before the sixth sample,
and the harness failed. It now flies slowly (acceleration_power 0.02, under
0.4 blocks a tick) down a longer corridor, and the guest takes 16 samples
in one ctl script (lan_e2e's Game.script returns every reply with every=True).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ArmorStand.BABY_DIMENSIONS keeps its own eye height rather than half the
type's, and EntityDimensions.eyeHeight is a float widened by getEyeY.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The parity client's chat, villager and furnace steps went on 1.5 s after
sending, so a busy server's late reply landed in the next step (or a click
carried a stale state id). Each step now waits until the server answered:
its chat line, both slot packets of a shift-click, and the furnace totals
the click changes (up to the same 30 s patience). The respawn window keeps
only the player's own entity packets: a squid's swim event or a mob coming
into view falls into it as each server's ticks fall.
Off the known-flaky list: it passes under 2.5 s server stalls every 5 s,
where main's client fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verify passed; chunk_generation, worldgen, worldgen_random, save_roundtrip and world_reopen pass locally on this head plus #2481; pr_ready --quick green. Perf fix, no label.
The worldgen bundle, the loot tables and the registries a server sends are
read on a helper thread from the moment the player opens Singleplayer or
Create World, so a world opening finds them ready. A world opened meanwhile
waits for the part being read. If the player goes back to the title or to
Multiplayer and opens no world for a minute, the preload is dropped: the three
caches can now let go of what they hold.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host fetches the commit from GitHub (nothing else is sent), keeps one
checkout and target per worktree, runs the command niced in its CI slice on
CPUs 4-7 with scratch homes, prepares vanilla data for harness commands as
CI does, and streams the output back. One run per name at a time; no lock
leaks into daemons the run starts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server put every player new to the world exactly on the world spawn.
On the server thread the host's own player joins that way too, so the host
and each new guest stood inside each other: the guest's client targets
the host's player (its eye inside that box, as vanilla's getEntityHitResult
does), so it could open no crafting table, chest or furnace, and Entity.push
never parts two players with the same centre. A dedicated server stacked
its new players the same way.
Vanilla places each new player with ServerPlayer.adjustSpawnLocation:
PlayerSpawnFinder.findSpawn, a free column at a random offset within the
respawn radius. Game::server_player_spawn does it for a server's new
players with the window's own finder, passing over the candidates whose
chunk isn't loaded yet (vanilla waits for them; a server tick can't).
lan_e2e's new check `spawned` asserts that the host and the guest join on
different spots, then has the guest open a crafting table, a chest, a
furnace and a stonecutter where it joined.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A title wider than the panel ended at its edge mid-word ("Recipes: Oak P");
it now ends with vanilla's "..." (CommonComponents.ELLIPSIS).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pr_ready --quick passed #1895 while CI's clippy failed on an unused import in horizon-mods' tests: clippy on the
touched crates and their dependents gives them other features than CI's workspace-wide run (Cargo unifies
features over the packages it builds).
- clippy: CI's two commands as they are. The whole workspace runs with the game's code mods, and the server build
(horizon-client --no-default-features) runs when horizon-client is affected. Warm on the Mac mini, a change to
hud.rs took 14-16 s against 17 s before, and food.rs 22-32 s against 14 s. Each worktree's first run costs
about 3 min once.
- tests stay on the touched crates, with the features CI's workspace run gives them: horizon-client's code mods,
and horizon-mods' runtime, pack and index.
- The Python tests always run, as in CI: some read the crates, the roadmap or the workflows.
- The launcher steps run when CI's launcher job would (tools/ci/rust_changes.py's rule, which also covers the
root manifests and the check itself).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main for #2316 (the campfire fix: this head had #2015 without it, so dispenser and block_removal would fail parity). Clippy -D warnings on horizon-mods (all targets, all features) and on horizon-client --features mods --bins is clean. Requeued at 8ec47d641.
Level.isRainingAt takes Biome.getHeightAdjustedTemperature, which applies
the frozen oceans' temperature modifier and the height noise above the snow
level; reuse horizon-world's version instead of a simplified copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: turtles lay (`TurtleLayEggGoal` with the digging effect), `TurtleEggBlock` hatches, and a growing baby drops a scute. I pushed one fix: the home is set at spawn and kept on load (`finalizeSpawn`, `home_pos`), and breeding drops one orb of 1 to 7, drawn from the mother's random and only with `mob_drops`. After #2137 the gift loot event names its mob. I merged main into your branch (6ef61ac13): main's `Reinforcements` stays beside your `BlockBreakEffect`, and the grown scute now goes through #2137's `drop_gift_loot` (the turtle's id added to both `GiftLoot` events) behind your mob_drops check; clippy clean, same code as the tested tree. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Merged by Ludovic's AI agent (Claude). The loot tables now load before a world's first chunks are read, so a loot chest near spawn gets its roll. The new spawn_loot harness fails on main (0 stacks) and passes here with vanilla 26.3's exact roll, on the server thread and in-frame. I merged main after #2316; dispenser and block_removal pass again. Follows @FusselTV's #2073.
Jobs queued while the loading screen was up carried ring-order priorities,
far above the plain distances used once it closed, so they waited behind
every later job. The proto chunks they generated held back the decoration
of the chunks around the player: at render distance 32 in-frame, nothing
was decorated until every proto chunk had been generated. Queued jobs now
keep their column and bias and are ranked again from the current centre,
extra areas, band and loading screen when those change, as vanilla's
chunk task queue follows the current ticket levels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps main's Reinforcements beside BlockBreakEffect, and the turtle's grown
scute goes through #2137's drop_gift_loot (the mob's id added to its two
GiftLoot events) behind the mob_drops check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BreakDoorGoal` (the difficulty gate, 240 ticks, the crack progress that your last commit now sends to nearby players) and the vindicators' door rules in raids. I pushed one fix: zombies the game spawns now roll `canBreakDoors` (`random < special * 0.1`), as `Zombie.finalizeSpawn` does. One difference is left for later: crack packets go to players within 64 blocks, and vanilla's `destroyBlockProgress` uses 32. I merged main into your branch (ac4309d3b): #2162 moved the zombie attributes into a free function, and its leader bonus now goes through `set_can_break_doors`; clippy clean, door tests pass, same code as the tested tree. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
tools/parity_hosts.json's new `known_flaky` lists the harnesses that still flake for a cause not fixed yet,
each with its reason: dedicated_server (its Java client's 1.5 s step timer), remote_fireball (wall-clock
sampling) and world_reopen. One of them that fails runs once more from scratch, and a pass then counts.
Every retry prints a RETRIED line at the end of the run and in the CI job's summary, so a harness that
passes only on its retry stays visible on PR parity and parity-main alike. The existing fresh rerun of a
failure that reused oracle results is reported the same way. A known flaky harness skips that rerun and keeps
reusing oracle results, because flaking doesn't mean its cached oracle runs were wrong. Repeated runs
(--repeat, --flake-check) are never retried.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps #2184's glide wear with Unbreaking after Soul Speed's tick, the order
of LivingEntity.baseTick and aiStep (both draw from the level random), and
both the can_glide_through and soul_speed_blocks tags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Shulker.hitByShulkerBullet` teleports an open shulker and may split it, less likely the more shulkers are near. I pushed one fix: the split roll uses the level's random, as vanilla's `level.getRandom()` does. Your update (d7ec7b607: the open state is read at hit time, the split chance from the level's random) checked too: clippy clean, and the 79 harnesses `parity.py --changed-files` picks pass apart from the campfire cases #2316 fixes. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `PiglinAi.mobInteract` takes only gold ingots from a hand, and other loved items go to its inventory. One difference is left for later: the comparison of two items it likes equally isn't ported yet. Your update (e60cbe8d4: whole nugget stacks stay in the inventory, picked-up enchantments are kept) checked too: clippy clean, the 79 harnesses `parity.py --changed-files` picks pass apart from the campfire cases #2316 fixes, and lan `barter` passes in both modes. One gap left for later: vanilla can put a nugget stack in an empty main hand, and Horizon's hand slot holds one item. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LivingEntity.handleFallFlyingCollisions` hurts on a horizontal hit with `fly_into_wall`. Gliding stops on climbable blocks and with Levitation, and the elytra wears through `hurtAndBreak`, so Unbreaking applies. Your update (a4407a4bc: Unbreaking for guests, wall hits kept on the server copy) checked too: clippy clean, the 79 harnesses `parity.py --changed-files` picks pass apart from the campfire cases in `dispenser` and `block_removal` that #2316 fixes, and lan `elytra` passes in both modes. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
A head tested alone keeps failing harnesses that main already fixed. Since 10-25 evening, 9 of 21 failed PR
parity jobs failed only on such fixes: server_knockback (heads without #2114, 7 times), mob_ai's oracle heap
(without #2270), save_roundtrip (without #2212). #2160 needed 4 parity runs this way.
The job now merges main's current tip into the head and selects harnesses against that main commit, so only
the pull request's own changes count. A branch that doesn't change the runner scripts or the selection gets
main's, which replaces the two steps that copied main's vanilla-data.sh and parity.py in by hand. A head that
conflicts with main is tested alone, with a warning. The job summary names the main commit, and the check is
still `parity / parity` on the head.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Witch.aiStep` stands still while drinking (the -0.25 speed modifier) and only drinks an effect it doesn't already have, and `performRangedAttack` skips a potion the target already has. It merges after #2138, since both add fields to `Seen`. I merged main into your branch (f8415efe2): `Seen` keeps #2138's sitting/ridden fields beside your effects; clippy clean. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: damage that doesn't bypass armor (cactus, fire, lava, campfire, berry bushes) wears the armor through `hurtArmor`. Here the wear lands when the damage events drain at the next player tick; vanilla applies it inside `actuallyHurt`. That is close enough for now. #2144 merged first and conflicted here, so I merged main into the branch: the hurt_armor call this PR takes out goes, and main's Thorns code stays. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the `ExperienceOrb` pickup box, merging by count, and the lava bounce. Your last commit's fix for a merged orb picked up by a guest or a server's client (one count at a time, the client keeping the orb) matches what I found. I pushed one fix to the harness: whether orbs merge depends on entity ids, which Horizon doesn't share with vanilla, so a spawner case in `block_removal` merged here and not in vanilla. The oracle and the fixture now print a merged orb once per count, and that case passes. (`block_removal`'s campfire cases fail on main until #2316 lands.) Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `PiglinSpecificSensor` finds the nearest `#piglin_repellents` block (a soul campfire only while lit), and the piglin walks away from it. I pushed one rename: this PR and #2191 each added a `find_repellent` to piglin.rs, so this one is now `find_piglin_repellent`. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BlockItem.place` plays the place sound only when a block item places a block; an empty hand or another item plays none. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ConduitBlockEntity.serverTick` (the frame every 40 ticks, Conduit Power in range, the attack on the nearest enemy), and your last commit's server ticker, which runs without a renderer. One difference is left for later: the conduit's target isn't synced to remote clients yet. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the illagers' target goals skip baby villagers. One difference is left for later, older than this PR: the illagers' shared hurt-by list still includes Vex. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the evoker's vex summon and cooldowns, the fang lines and circle, the vex limits, and the wololo spell (blue sheep only, with `mob_griefing`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.drop` throws leftovers from the eyes, and `ArmorStand.swapItem` lets a creative player keep their stack. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `CactusBlock.entityInside` hurts any entity with `cactus` damage, and that destroys item entities. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ChestBlock.isChestBlockedAt`: a block above that is a redstone conductor, or a sitting cat on top. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the guardian's beam deals 2 more on Hard and 2 more from an elder, and squid and axolotl targets must be over 3 blocks away (`GuardianAttackSelector`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Chicken.aiStep` lays through `dropFromGiftLootTable(gameplay/chicken_lay)` with the chicken as `this`, which the table's conditions need, and snow and chorus flowers drop their loot when a player breaks them. One difference is left for later: an explosion's drops don't pass the exploding entity as `this` yet (snow broken by TNT). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
A thrown potion or experience bottle that breaks sends level events 2002 or
2007 with its colour and the splash sound (1053, 1054). Horizon spawned only
eight terrain crumbs, on the server's own side, so a server player saw
nothing.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the turtle helmet gives Water Breathing for 200 ticks while the eyes are out of water (`Player.turtleHelmetTick`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Bucketable.bucketMobPickup` for cod, salmon, pufferfish, tropical fish, tadpoles and axolotls, with the data each bucket carries (salmon size, tropical fish pattern and colours, the age only for axolotls and tadpoles) and the pattern names in enum order. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.findLightningRod` takes the closest lightning rod POI within 128 blocks, at the top of its column. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked: the hotbar now draws its stacks by reference. The output is the same, without a clone per stack each frame. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @Domanffe!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: a charged creeper's explosion makes a mob it kills drop its head, once per creeper (`Creeper.canDropMobsSkull` / `increaseDroppedSkulls`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
#2162 moved the zombie attributes into a free function; its leader bonus now
goes through set_can_break_doors like this branch's other door flags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Zombie.hurtServer` on Hard rolls `spawnReinforcementsChance` and tries 50 spots around the target (`checkSpawnRules`, no player close, unobstructed), and both zombies lose 0.05 of the chance. Two small differences are left for later: the roll is drawn at the end of the tick rather than inside the hurt, and the new zombie draws one extra yaw. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: Infinity's `ammo_use` effect only covers `minecraft:arrow`, so tipped and spectral arrows are used up. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
Every item sits in the item panel beside the inventory and container
screens, with a search box (words in the name or id, @namespace, #tag).
R or a click shows an item's recipes, U or a right click its uses, with a
tab per station; Back returns through the pages. + beside a crafting
recipe the player knows fills the grid as the recipe book does. In
creative mode, with "Click gives items" on, a click gives one item and a
right click a stack. The keys are rebindable in Controls.
Written from vanilla 26.3's recipe book and screens and Horizon's design
system only. On servers that are not Horizon it shows the game's own
26.3 recipes; known recipes and + follow the server's recipe book.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sprinting player kicks up dust of the block under their feet every tick
(Entity.spawnSprintParticle). Horizon drew none, for the window's player
or for the other players it sees.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FishingHook.canHitEntity` hooks item entities too, its luck is the owner's luck attribute plus Luck of the Sea, and `fish_caught` counts the catch. One difference is left for later: the hook takes the first overlap rather than the closest one on its path. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
The property was parsed and ignored. The tracking range of each entity type is now scaled by it on a dedicated server, like DedicatedServer.getScaledTrackingDistance.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ButtonBlock.checkPressed` presses a wooden button while an arrow is inside its shape and keeps it pressed as long as one is there. One difference is left for later: wooden pressure plates and tripwires don't count arrows yet. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `TargetBlock.onProjectileHit` gives a signal of 1 to 15 from how close the hit is to the face's centre, for 20 ticks from an arrow and 8 from anything else, with the `target_hit` trigger and stat. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `DaylightDetectorBlock.updateSignalStrength` (sky light minus darkening, the sun-angle curve, the inverted mode) and its update when used. Main conflicted with its new campfire block entity, so I merged main into the branch, keeping both block entities. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: Piercing sets the arrow's `pierceLevel`, and `AbstractArrow.onHitEntity` remembers the entities it went through and goes on until the level runs out. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: a thrown trident wears through `hurtAndBreak`, so Unbreaking applies, and `TridentItem.releaseUsing` doesn't launch a riding player with Riptide. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
As the creative inventory does: a count over the item's stack size (64, 16
or 1) is cut to it, so a mod can ask for a full stack without knowing it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: channeling.json summons lightning at the trident's hit while it thunders and the target sees the sky. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `VillagerMakeLove.takeVacantBed` asks the POI manager for a free home within 48 blocks (a sphere, closest first) and keeps only a bed a path reaches. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
A chunk's STRUCTURE_STARTS are the starts made in it
(StructureManager.getAllStarts), whether or not one of their pieces
reaches it. own_starts filtered the chunk's references, which only hold
starts with a piece in the chunk, so a start whose pieces all lie next
door (seed 8675309's giant ruined portal in chunk -26, 23) was not saved.
The proto chunk now keeps the starts made at its position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `WorkAtComposter` composts the farmer's seeds through `ComposterBlock.insertItem` and makes bread from wheat. I pushed one small fix: the `ComposterBlock.addLayer` doc comment had moved off its function. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
WorldGenRegion.setBlock gives every block that has a block entity a new
one, so vanilla saves the sculk sensors, catalysts and shriekers of the
deep dark and potent sulfur with their block entities. Horizon only
saved block entities that a structure or feature placed data for, and
these kinds have none, so a Horizon-made world had none of them (3,035
sculk sensors, 753 catalysts, 329 shriekers and 21 potent sulfur over
three 1,024-block seeds). A finished column now adds vanilla's defaults
(block_entity_nbt::canonical) for every such block: as the modelled
block entity where Horizon has one (sculk), as raw NBT otherwise.
Potent sulfur gets its block entity type.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With force-loaded levels ticking without players (#1525), Horizon placed
the exit portal as soon as the End's main island was loaded. Vanilla
only places it from scanState and the dragon's death, both with players
in the fight, so a pre-generated End has no portal until someone
arrives.
test_forced_end_parity force-loads the End's arena on vanilla's and
Horizon's dedicated servers with nobody online and compares what is
around 0, 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SpikeFeature.placeSpike adds each crystal to the WorldGenRegion, so a
pre-generated End holds its ten crystals before anyone ticks it.
Horizon's decoration dropped spawn_end_crystal and the game placed the
crystals later, once the arena was loaded and ticking. An End generated
with ticks frozen, as a seed generator does, had none. The decoration
region now writes the crystal like any placed entity; the game's own
placement still skips a spike that has its crystal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BrushableBlock.tick makes the falling block entity with disableDrop, so
vanilla breaks it on landing (onBrokenAfterFall: level event 2001 and
the block_destroy game event) and
places nothing. Horizon landed it as a plain block without its
archaeology block entity. Seen in pre-generated worlds: suspicious gravel
of cold ocean ruins that sits over water fell during the first ticks and
landed one block lower without its block entity, so with no loot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chunks no player ticket reached were decorated nearest the centre first.
Vanilla's ForceLoadCommand adds a FORCED ticket per chunk, x-outer and
z-inner, and the FULL tasks run in that order, so a chunk decorates
before its neighbour at higher x or z. Both orders agree only in the +x,
+z quarter of the map. Elsewhere features crossing chunk borders
overwrote each other the other way round: a geode next door ate a trial
chamber's dispensers, sculk or a clay pool closed a dungeon so it was
never placed, and ore, stone and tree blobs swapped.
The forceload command now ranks the chunks it adds in vanilla's order
(decoration_order::forced_order, which shares its scheduler with
vanilla_order); chunks without a rank still go last, nearest first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Creeper.mobInteract` ignites with any `#creeper_igniters` item. A fire charge is used up, flint and steel takes damage, and both play their own sound. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AbstractSkeleton.reassessWeaponGoal` sets the bow goal's interval to 20 ticks on Hard and 40 otherwise. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
Brings #2316 (the campfire block entity fix): CI's parity checks out this
branch's head, so dispenser and block_removal need it here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.explode` turns `ExplosionInteraction.MOB` into KEEP when `mob_griefing` is off, and `EnderMan`'s take and leave block goals need the rule too. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.doSweepAttack` hits the living entities in the target's box inflated by (1, 0.25, 1), not the player, the target or allies, and not marker armor stands. I pushed one fix: the distance bound is vanilla's fixed `distanceToSqr(entity) < 9.0`, not the player's reach. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
A row of tabs above the recipe pages shows `set-recipes`' stations (their
item in a slot, a white frame on the selected one). A click sends `tab(i)`
to the mod, R/U over a tab works like over any slot, and horizon-ctl lists
them as "<name> tab". Changing tab resets the page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: rabbits breed (`Rabbit.isFood`), damage ends love (`Animal.hurtServer` resets `inLove`), the horse and wolf `canMate` checks, and the breeding orb only with `mob_drops` (`Animal.finalizeSpawnChildFromBreeding`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.hurtServer` scales damage that `scalesWithDifficulty` from a living non-player: none on Peaceful, `min(d / 2 + 1, d)` on Easy, `d * 3 / 2` on Hard (so a creeper's blast scales, and TNT a player lit doesn't). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SetStewEffectFunction` keeps the effect it rolled, with the duration times 20 unless the effect is instant. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Merging ahead of the queue. verify, launcher and CI parity pass, and dispenser and block_removal are back to 4000/4000 and 6000/6000. This turns main green again after #2015.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SlimeBlock.stepOn` slows a walker that isn't sneaking when `|dy| < 0.1`, by `0.4 + |dy| * 0.2`. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `/weather` sets the weather whether or not `advance_weather` is on. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerPlayer.startSleepInBed` (creative skips the monster check; the block above must be free; the reach box), and `ServerLevel.tick`'s morning skip with the weather reset. One difference is left for later: the monster box is centred on the player, and vanilla centres it on the bed. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FireBlock.tick`: burn-out chances, ignite odds scaled by difficulty, and the spread box (-1..1 sideways, -1..4 up). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LightningBolt.spawnFire` only on Normal and Hard and with fire spread on, as here. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
Main's thorns (#2144) sit where this branch took out hurt_armor; the armor
now wears through the damage pipeline and main's thorns stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `EnderDragon.checkCrystals` looks for crystals in its box grown by 32 and heals 1 every 10 ticks, capped at max health. Main moved the crystal search into `nearest_crystal_to`, which the server thread's client uses too, so I merged main into the branch and put the grown box there. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `KeyMapping.click` and `set` act on every mapping bound to the key, as here, and your second commit stops a key from replaying a press when two mappings share it by default. #2207 fixes the same issue; I kept this one because it came first and changes less. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ShovelItem.useOn` puts out a lit campfire (`CampfireBlock.dowse`) with its sound and smoke. One difference is left for later: clients of the server thread don't see that smoke yet. Main conflicted with its new `campfire` module (cooking), so I merged main into the branch, keeping both modules. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.tick` keeps ticking a level that has forced chunks, even with no players and past the 300-tick empty timeout. `lan_e2e.py --server-thread --check forced-levels` passes. Main conflicted in lan_e2e.py, so I merged main into the branch, keeping both checks. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @rdbtCVS!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.runBlockEvents` broadcasts each handled block event, and `ClientLevel.blockEvent` re-runs `triggerEvent` on the client, which builds the moving piston it ticks and draws (`deathTicks`, client-side `finalTick`, the 2001 effect for destroyed blocks). I pushed one fix with a test. The client's re-run also ran the server's neighbour logic: a block it moved could start another piston on that client alone, and vanilla calls `checkIfExtend` only server-side. Its `playSound(null, ...)` also played the piston and note sounds a second time, next to the server's; `ClientLevel.playSeededSound` plays only when the excluded player is the local one. In game on the server thread, a piston that pushes a redstone block under a second piston leaves both clients' blocks as the server has them. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @socksprox!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: Thorns' `post_attack` effect damages the attacker (the shooter for a projectile). Main conflicted in `lan_pvp.rs` because it already had `Strike.attacker`, so I merged it, keeping main's critical-hit code and then your Thorns loop. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
Brings #2212 (namespaced furnace RecipesUsed), which save_roundtrip needs:
CI's parity checks out this branch's head, not a merge with main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `HoglinSpecificSensor` finds the nearest `#hoglin_repellents` block (8 sideways, 4 up and down), and `HoglinAi` walks away and stays passive for 200 ticks. I pushed one rename: this PR and #2192 each added a `find_repellent` to piglin.rs, so this one is now `find_hoglin_repellent` and the two can merge together. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked: the crack overlay keeps its UVs aligned from one stage to the next. Its ignored GPU test passes here (macOS, Metal). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @rdbtCVS!
Since campfires got their block entity (#2015), the container lookup took
it for a zero-slot container: a dropper facing one kept its item instead of
dropping it, as vanilla's HopperBlockEntity.getContainerAt finds no Container
there. The dispense fixture also filled campfires through the generic slots,
which a campfire doesn't have, so block_removal saw them empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
recipes.place(book-id, all) is the recipe book's click: in the open
inventory's or crafting table's grid it sends ServerboundPlaceRecipePacket
on a server, or places it in this world, refused as handlePlaceRecipe
refuses it (no crafting screen, a recipe the player's book doesn't hold, an
impossible one); a recipe missing items shows as a ghost. recipes.give(stack)
gives a creative player the stack as the creative inventory does; a server
hears it as SetCreativeModeSlot through the inventory's sync. Both answer at
once from the tick's snapshot (placing, creative, known) and take at most 36
calls a hook call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked: the renderer that replaces this one shares its instance, adapter and device, so it can take the configured surface instead of creating a second one for the same window. Wayland allows only one tearing-control object per surface, and the reporter's log shows the fresh surface coming back lost. I can't reproduce the Wayland/RADV crash here (macOS), so this is checked from the code and the log. On macOS the game still draws after the renderer is rebuilt (screenshots before and after). Fixes#1959. Main's HDR output conflicted in recreate_surface, so I merged main into the branch: the reused surface now goes through main's Renderer::configure too. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
Full trial (run 40341431094): 342/348 in 96.9 min; save_roundtrip,
remote_fireball and server_player_sculk passed on a rerun with vscreen and
fewer jobs; these three did not.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged by Ludovic's AI agent (Claude). setblock and data now keep a container's LootTable and LootTableSeed until its items change, as vanilla does. command_exec matches the 26.3 oracle (870/870), and in game the chest opens with vanilla's exact roll, in-frame and on the server thread.
browser.set-recipes replaces the panel's items with the mod's recipes,
laid out with the game's slot, furnace arrow and flame sprites and small
buttons: a crafting grid, one input over a flame, one input, three inputs
or two, then an arrow and the result, a note under it and the action
button beside the result (under it when the panel is narrow). Slots cycle
through their stacks once a second. As many recipes as fit the panel's
height make a page, with the recipe book's arrows; a title row holds a
Back button and the title. The same title sent again keeps the page.
Clicks on the slots, the action button, the arrows and Back reach the mod;
tooltips and the mods' keys work on the recipes' stacks as on the items.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod gives the recipes to show (a layout: crafting grid, cooking, single
input, smithing or brewing; the slots, each cycling through its stacks;
the results; a note; and an optional action button, enabled or greyed)
under a title; an empty list shows the items again. The game pages them
and sends the slot-clicked, action, page and back events.
At most 256 recipes, 9 slots each and 64 stacks a slot. Same level as the
rest of browser (5): none of it has shipped yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A command's tag went through decode_block_entity, which has nowhere to put
LootTable, so `setblock ... chest{LootTable:...}` made an empty chest
(`data get` showed `Items: []`). An empty container given a LootTable is
now rolled as a loaded chunk's are, and the roll is kept with its chunk
(RolledLoot), so it saves, and `data get` shows, LootTable and
LootTableSeed until its items change (tryLoadLootTable / trySaveLootTable).
`data merge` and `data modify` go through the same path.
command_exec: setblock a seeded loot chest, then data get it whole and its
LootTableSeed (870/870 match the 26.3 oracle).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The client branch of Squid.aiStep (body pitch/roll from the server's velocity, the tentacle cycle, entity event 19 resetting it) matches the 26.3 bytecode. The new `squid_client` harness passes against the real Squid oracle on an integration tree with the other open contributor PRs. I merged main into the branch and stubbed in test_remote_motion's fixture what remote_motion now also reaches (dragon, fireballs, thrown potions, eyes of ender), so that test runs again. Fixes#1684 is right. Thanks @ESHAYAT102!
Main's HDR output configures the surface through Renderer::configure; the
reused spare surface goes through it too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Before/after on the Mac mini (M4 Pro) with the creative inventory open, two runs each, fast profile, `horizon-ctl bench`. Main: CPU frame 0.81 to 0.91 ms on the building/combat tabs and 1.32 to 1.47 ms while searching (664 to 742 fps). This PR: 0.44 to 0.49 ms on building/combat and 0.49 ms while searching (1122 to 1125 fps). The panel pixels match main. After #1990 made the search an EditBox, I merged main in, and the shown items now key on its value; in game, typing "diamond" narrows the search tab from 45 to 17 stacks. `item_model`, `creative`, `container_screen` and `hud` pass, and so do the creative unit tests (18). Fixes#1953 is right. Thanks @kacpherek!
🤖 Agent: Merged by Ludovic's AI agent (Claude). Two tests in tests/ errored only on macOS (a python3-less PATH for the oracle shim, and /var vs /private/var); the suite now passes on the Mac mini, 311/311.
Whether two orbs merge (ExperienceOrb.tryMergeToExisting) depends on entity
ids, and the fixture's ids aren't the oracle's: a spawner case merged in
Horizon and not in vanilla (3 orbs against 4). Both sides now print an orb
once per count, which compares what was awarded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The panel sits right of the inventory or container window, from the top of
the screen to a search box at its foot, in up to 12 columns of the game's
own item slots, with the recipe book's page arrows. It shows on the
survival inventory and every container screen (not the creative one) while
a running mod has items in it, and hides when there is no room for two
columns. Clicks on it never throw the carried stack; the carried stack and
tooltips are drawn again above it.
The mod hears clicks on items, the search text as it changes, and its own
keys pressed with the pointer on an item, the panel's or a slot's. The
search box takes every key but Escape while focused, as the recipe book's.
Each set-items is turned into stacks once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new interface under capability ui. browser.set-items lists the items of
a panel the game shows right of the inventory and container screens (an
empty list hides it); browser.poll returns what the player did since the
last poll: a click on an item, the search text, or one of the mod's own
keys pressed over an item (a slot's or the panel's), with that stack. The
slot-clicked, action, page and back events are there now for the recipe
pages to come, since adding variant cases later would break mods.
At most 8192 items and 256 waiting events. The game side is the next PR.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OracleReuseTests ran the shim through `#!/usr/bin/env python3` with a PATH
whose Python folder is Homebrew's, which names it only python3.13: the
shim wasn't found (127). The test now gives that PATH a python3 of its own.
test_report_rejects_sources_changed_during_a_run compared a resolved source
path with an unresolved temp root, and macOS's /var is /private/var.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote_motion now also hands motion to the dragon, its fireballs, thrown
potions and eyes of ender; the extracted fixture needs those to compile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Forced chunks are now kept in each dimension's data/minecraft/chunk_tickets.dat as 26.3's TicketStorage writes them, and the persistent tickets Horizon doesn't simulate are kept too. A world with /forceload areas round-trips with vanilla 26.3, and the save harnesses pass on an integration tree with the whole save series. I merged main into the branch after #2070 and #2288 and moved the new test to the end of save.rs's tests, so the other save PRs don't collide with it (save:: 27/27). Fixes#1527 is right. Thanks @rdbtCVS!
🤖 Agent: Merged by Ludovic's AI agent (Claude). /recipe now changes each target's own recipe book and tells that player's client, as RecipeCommand does through ServerPlayer.awardRecipes/resetRecipes. lan_e2e recipe_targets fails on main and passes here, in-frame and on the server thread; recipe_removal and command_exec still pass. Thanks to platform-2 for finding it.
🤖 Agent: Merged by Ludovic's AI agent (Claude). This lands @FusselTV's #2074 (write points of interest), verified by a vanilla 26.3 round trip; the save harnesses pass. The only additions are merges of main (after #2070 and #2288); save:: passes 29/29. Thanks @FusselTV!
🤖 Agent: Merging. verify passes, and the new launcher job passes on this head (fmt, clippy -D warnings, lib tests). On the earlier head it had caught main's E0061 before #2281. The Python tests show the same 2 environment errors as main.
cmd_recipe changed self.level.recipe_book for every target, so
`/recipe give|take <other player>` changed the book of whoever ran it (or
the dedicated server's own) and the target's client heard nothing. Each
target now runs with its state swapped in (for_player), as
ServerPlayer.awardRecipes/resetRecipes do on the player's own
ServerRecipeBook: the guest's book changes and its client gets
ClientboundRecipeBookAdd/RemovePacket.
lan_e2e recipe_targets: the host gives and takes the guest's recipes and,
on the server thread, the guest gives the host one; each open book must
show exactly its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged by Ludovic's AI agent (Claude). This lands @FusselTV's #2073 (unopened containers keep their loot table), verified against vanilla 26.3: seeded loot 48/48 in both single-player paths, and the save harnesses pass. The only addition is a merge of main after #2070. Thanks @FusselTV!
Prepending tools/ made unittest discovery import tools/test_decoration_parity.py
in place of tests/test_decoration_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main to resolve a conflict in game/remote.rs (main's boss-overlay reset beside this PR's QuietCommands event; both kept). Clippy with mods on is clean. Requeued at the new head.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Item.canDestroyBlock in creative (swords, maces and tridents can't break blocks) matches the 26.3 bytecode, on the player's prediction and on the server. On the integration tree, `lan_e2e --check creative_tools` passes in-frame and on the server thread, and the new `creative_mining` harness passes against its oracle. I merged main into the branch twice; the second time, lan_e2e.py and the probe list keep both sides, every def checked against its source branch, and client_probe and the bins compile. Fixes#2035 is right. Thanks @ESHAYAT102!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AreaEffectCloud` syncs `DATA_RADIUS` (8), `DATA_WAITING` (9) and `DATA_PARTICLE` (10), waits 20 ticks (dragon clouds too), and `clientTick` is ported draw for draw. `DragonFireball`'s trail is `dragon_breath`. One fix pushed: `AbstractHurtingProjectile.tick` calls `applyInertia` before the move, so the client's fireball now accelerates and then moves by the new velocity, with `Vec3.normalize`'s zero for a tiny vector. Main conflicted with #2086 (thrown projectiles between server updates), so I merged it, keeping both: the thrown and eye path first, then the dragon fireball's. `lan_e2e.py --check particles` and `objects` pass in both modes. Part of #2083, as it says. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Chunks now write the structure starts and references they hold, so vanilla keeps the structures (and their spawns) after the round trip. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
🤖 Agent: merging. playhorizon.dev already deploys from ludovic111/horizon-site (verified page by page); this drops website/ and moves tokens.css, roadmap.json, the benchmark fixtures and the web icons where the site's build and the tests read them. verify passed; pr_ready --quick and the telemetry server's tests pass locally.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BaseFireBlock.animateTick` (three large smoke above, or two per burnable side when the block below neither burns nor is sturdy up, in that order and with the `* 0.1F` float edge), `SoulFireBlock.canBurn` always true, and `Level.destroyBlock` → `spawnDestroyParticles` → `BaseFireBlock`'s empty `spawnDestroyByEntityParticles`. `fire.igniteOdds` is in block-behaviours.json at the top level, as read. I added one rule: `FireBlock.getIgniteOdds` is 0 for a waterlogged state, so such a neighbour no longer counts as burnable. This and #2082 (the player's predicted break) cover different paths, so both stay; #2092 was already closed in favour of this one. In game, fire and soul fire show their large smoke above them. Thanks @socksprox, and @kacpherek for #2092! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `NetherPortalBlock.entityInside` takes any entity that `canUsePortal(false)`, and boats and minecarts travel through `teleportCrossDimension` with their data, as items and mobs already did here. A ridden vehicle stays out of this PR's scope (the player-and-vehicle case is #1486). `lan_e2e.py --check vehicles`, `boats` and `minecarts` pass in both modes. Fixes#1516. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ItemFrameRenderer.getBlockLightLevel` gives a glow frame's model at least block light 5 (the item and map inside stay full bright), and the frame model takes the entity shader's directional shading from its face normals. My screenshot run couldn't place a frame, so this rests on the code and on `lan_e2e.py --check frames`, which passes in both modes. Fixes#1436. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `EndPortalBlock.entityInside` calls `showEndCredits` on a player in the End without `seenCredits`. That sends `WIN_GAME` with 0.0 once (`wonGame`), and `handleClientCommand(PERFORM_RESPAWN)` while `wonGame` respawns the player in the Overworld keeping its data. `seenCredits` round-trips through playerdata. One difference is left for later: vanilla takes the player out of the End level (`removePlayerImmediately`) while the poem rolls, and here it stays in the level. `lan_e2e.py --check portals` passes with the host on its server thread (I pushed one fix to the check: it closes the credits on the host's first End exit). Fixes#1507. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
#2171 takes level 3 (command.run-quiet), so the world interface's functions
take level 4. The workspace members keep main's horizon-hdr and this branch's
beacons example.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The remaining legacy structure pieces now save their vanilla fields. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Template pieces now save as vanilla's TemplateStructurePiece does. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ItemCost.STREAM_CODEC` is item, count, then `DataComponentExactPredicate.STREAM_CODEC`, a list of typed components with no removal count, as now written and read. Its verify failed only on rustfmt (item_sync.rs), which I pushed. Main conflicted in item_sync.rs's tests (the new bundle test), so I merged it, keeping both tests. I also took the codec round trip back out of the trading probe: the probe's output is what the harness compares with vanilla, the round trip is already covered by your two unit tests, and its `unwrap` would abort the probe on any cost whose components name an entry the stub registry lacks. `lan_e2e.py --check hero-trading` passes in both modes. Fixes#1948. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
playhorizon.dev now deploys from the private ludovic111/horizon-site, which holds website/'s history. The files
its build still reads from here move out of website/:
- the design tokens to crates/horizon-theme/tokens.css (xtask tokens writes them, tokens_css_is_up_to_date checks them);
- the roadmap's JSON to roadmap/roadmap.json (tools/roadmap.py build);
- the benchmark fixtures to crates/horizon-telemetry-server/tests/fixtures/benchmarks/ (the site keeps a copy);
- the web icons to docs/media/brand/web/ (xtask icons).
pr_ready loses its website area and the tokens special case; rust_changes, the roadmap's links and every doc
that pointed into website/ follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Guardian.DATA_ID_MOVING` (16) and `DATA_ID_ATTACK_TARGET` (17, the target's entity id, 0 for none) are synced, `onSyncedDataUpdated` zeroes `clientSideAttackTime`, and the client's `aiStep` advances it up to `getAttackDuration` (80, elder 60), which drives the beam. The in-frame tick now shares `client_tick`. Main conflicted (the bat flags from #2003, the sonic and cushion checks), so I merged it into the branch, keeping both sides. `lan_e2e.py --check guardian-beams` passes with the host in-frame and on its server thread (I pushed one fix to the check: the host reads the guardian from the level that ticks it). Fixes#1491. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
A loaded container with a loot table is rolled as its chunk is decoded, but
the loot tables were loaded lazily by the first level tick. The spawn chunks
are decoded while the world opens, so a loot chest in them came out empty and
lost its table on the next save. WorldRequest::prepare, which every world
opens through (in-frame, server thread, dedicated server), now loads them
first.
spawn_loot: vanilla writes a flat world with a seeded loot chest by the
spawn and rolls it; Horizon opens it in a fresh process, in-frame and on the
server thread, and must hold the same stacks in the same slots. Without the
fix both modes hold nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Block entities placed in proto chunks now save with their id, as vanilla's ProtoChunk does, so vanilla loads them. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Nether and End chunks now save with their own min_y/height and sky light, as ChunkSerializer does per dimension: vanilla reads them back with every section in place. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
🤖 Agent: Merged by Ludovic's AI agent (Claude). This keeps proxy credentials out of the launcher's messages and fixes the launcher build that broke on main (download::agent call in browser/horizon.rs). Launcher lib tests 137/137, horizon-port-pack 68/68. Thanks again to @zhykowo for the proxy support in #2059.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ContainerEntity.isChestVehicleStillValid` is `!isRemoved() && player.isWithinEntityInteractionRange(getBoundingBox(), 4.0)`, which is entity interaction range (3, creative 5) plus 4 measured from the eye to the box, as here. The menu had closed on the tick it opened because the placeholder key was checked as a block. This is also the Discord report of a chest minecart that plays its sound but opens no screen. `lan_e2e.py --check minecarts` passes in both modes, and through horizon-ctl a chest minecart's screen is still open 60 ticks after it opens. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). A trial spawner with nothing to spawn no longer writes an empty spawn_data, matching TrialSpawnerStateData's codec. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The item_name component now sits on items and tooltips as vanilla's Item.getName/getStyledHoverName read it (custom_name first, then item_name, then the translation key). I pushed d983ef89b so item_name stays a component and is not folded into custom_name. `tooltip_display`, `lore_tooltip` and `firework_tooltip` pass on an integration tree with the other open contributor PRs. Fixes#2039 is right. Thanks @lazy-importer!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Campfire cooking (CampfireBlockEntity.cookTick/cooldownTick, placing food, dropping the result, the arm swing) matches 26.3, and food cooks in game on the server thread and in-frame. Since campfires now tick, the save round trip saw the fixture's lit campfire advance its CookingTimes between load and save, as the lit furnace already did. I added that one ignore rule (ba797f60a); `save_roundtrip` then passes with 0 unexplained differences. Thanks @vlesmentie!
verify's cargo runs never build launcher/ (its own workspace), so #2059 and the
registry ratings commit each passed alone and left main's launcher failing to
compile (E0061). The new launcher job runs its rustfmt, clippy -D warnings and lib
tests when a pull request changes the launcher, a crate it builds or the root
manifests, and on every push to main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved the crystal search into nearest_crystal_to, which the server
thread's client uses too; the dragon's grown box now lives there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Sculk partner banner in the launcher (Servers and Home), as Ludovic asked. Launcher fmt, clippy -D warnings and tests pass; checked in dark and light on the Mac mini; verify passed on this head.
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FireworkRocketEntity.explode` broadcasts entity event 17 before damage and `discard`, and `handleEntityEvent(17)` calls `createFireworks` with the rocket's movement and its item's explosions, so sending the event before the tracker drops the rocket is the right order. Main conflicted (fireworks.rs, lan_e2e.py), so I merged it, keeping main's rocket damage and your `remote_firework_explosion`. `lan_e2e.py --check fireworks` passes in both modes. Fixes#1478. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
An address the launcher can't use as a proxy is printed to stderr by
horizon-port-pack and shown under the Settings field or by `settings set`:
a user name and password in it now read `***`.
The registry's star ratings called download::agent() without the address
it now takes (#2059 and the ratings landed side by side), so the launcher
didn't build.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AbstractRecipeBookScreen.mouseClicked` offers the click to `recipeBookComponent` before anything else, and the tabs sit left of the book, outside the strip `hasClickedOutside` counts, so the outside branch has to ask the book first. `lan_e2e.py --check recipes` passes in both modes. Fixes#2091. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
🤖 Agent: Gate check by Ludovic's AI agent (Claude). I merged main and added `crates/horizon-hdr` to the workspace, and pushed caae02641 so that native material packs keep a light-emitting translucent face (nether portal) at its own alpha. HDR stays off by default. With it off, screenshots match main within noise, and the frame time does too (A/B, two runs each: p50 0.326 to 0.329 ms vs 0.317 to 0.345 ms). `section_mesh` passes. HDR on was not tested here (no HDR display on the Mac mini). Thanks @senjinthedragon!
#2191 and #2192 each add a `find_repellent` to piglin.rs, one for
hoglins and one for piglins; merged together they would collide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#2191 and #2192 each add a `find_repellent` to piglin.rs, one for
hoglins and one for piglins; merged together they would collide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). With the default server thread, the End's dragon is now drawn (screenshots in the End: invisible on main, visible with this PR), and attacks on its parts reach the server. `server_dragon` passes on an integration tree with the other open contributor PRs. Fixes#1530 is right. Thanks @vlesmentie!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). rdbt's approval counts as the first review (trusted reviewer). In the integration tree with the other open PRs, `lan_e2e --check thrown` and `--check projectiles` pass in-frame and on the server thread, and the `pearl_death` harness passes. Fixes#1826 is right. Thanks @kacpherek!
Campfires now tick, so the fixture's campfire advances its CookingTimes
between load and save, as the lit furnace does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sculk, a server host, sponsors Horizon. Its banner (a 50 KB WebP,
1200x441, bundled with include_bytes) sits under a muted "Partner"
label at the end of the Servers page and of Home, below Play. A click
opens https://my.sculkhosting.com/ref/horizon; hovering lifts it 2 px
and turns the label to ink; it is reachable with Tab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zombie.finalizeSpawn calls setCanBreakDoors(nextFloat() < special * 0.1)
after the baby roll; the game's spawn finalize never rolled it, so only
the finalize probe's zombies could break doors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). This needs no decision from Ludovic: every field now edits at the caret, as vanilla's EditBox does. rdbt's line-break review: the book page is a MultiLineEditBox in 26.3 (javap), so I pushed MultilineTextField's own word rule. getPreviousWord/getNextWord split words at any Character.isWhitespace and start from the cursor clamped to the last character. The single-line EditBox.getWordPosition keeps counting spaces only. I also merged main in twice (multiplayer.rs labels, stations.rs). Integration tree: `lan_e2e --check books`, `signs` and `anvil` pass in-frame and on the server thread, the `creative` and `container_screen` harnesses pass, and the ui/creative/title/clipboard unit tests pass. Thanks @Joansitoh!
A client's copy of a block event (`client_block_event`) ran the server's
neighbour logic: a block it moved could start another piston on the client
alone (`PistonBaseBlock.checkIfExtend` is only called server-side), and the
re-run's `playSound(null, ...)` played the piston and note sounds a second
time next to the server's (`ClientLevel.playSeededSound` plays only for the
local player as the excluded one).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). I checked CushionItem.useOn and Cushion.interact against the 26.3 bytecode with rea_vanilla and pushed three leftovers. (1) An occupied cushion now returns PASS, as Cushion.interact does. (2) A cushion placed in fire burns at once (destroyIfInFire). (3) Placing always posts the entity_place game event before consuming the item. I also merged main into the branch (lan_e2e.py). The suffocation check matches vanilla's block definitions except closed shulker boxes, which are approximated as always suffocating (noted in a comment). Integration tree: `lan_e2e --check cushions` passes in-frame and on the server thread, and the `cushion_placement` and suffocation harnesses pass. Thanks @ESHAYAT102!
recipes.items() lists every item in registry order, with its name in the
player's language and its tags. recipes.all() lists every recipe: crafting
and furnace ones from the recipe book's own entries (vanilla's displays and
display ids), then campfire, stonecutting, smithing and brewing (display
only). Stacks point into the item table by index. recipes.known() gives the
book ids the player's recipe book holds, from the client book in every mode,
so on a server it follows the server's book.
The game builds the tables once, the first time a running mod reads them.
Experimental, new functions only. Smithing::Transform now keeps its recipe
id.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The launcher contacts no new host: the existing requests (releases, Mojang files, Modrinth, CurseForge, the mod registry, pictures, Microsoft sign-in, Tailscale's API, modpack jars) now go through the player's HTTP proxy, or HTTPS_PROXY/HTTP_PROXY/ALL_PROXY when unset. Loopback and NO_PROXY hosts stay direct, and the Servers page's local API never uses the proxy. Tokens (Microsoft, Tailscale) only cross the proxy inside TLS (CONNECT), so the proxy can't read them, and none are logged. rdbt's NO_PROXY review is addressed: `horizon-port-pack`'s agents apply the same rules (`no_proxy_lists_hosts_and_their_subdomains`). On Linux at da54f988d: launcher lib tests 132 passed, `cargo clippy --all-targets -D warnings` clean, `horizon-port-pack` 68 tests passed and clippy clean. Labelled feature. One small note for later: an unusable proxy address is printed to stderr as typed, so a `user:pass@` in it would show in the log. Thanks @zhykowo!
🤖 Agent: Gate check by Ludovic's AI agent (Claude). `tools/bundle_tooltip_e2e.py` passes in-frame. On the server thread (what players run) it showed every bundle full, holding more bundles, so I pushed two commits: (1) BundleContents' stream codec (component 52, a list of ItemStackTemplate, checked with javap) plus sending it to clients in `network_stack`; (2) `Hub::stack` gave every stack inside an item the outer item's id, which also turned a crossbow's charged projectiles into crossbows on clients. It is fixed now: each inner stack looks up its own id. With both commits the e2e passes on the server thread and in-frame (306 px tooltip frame; main shows 117 px and fails). The screenshot shows 32 stone with a half-full bar, as in vanilla. Unit tests: `bundle_contents_are_item_stack_templates` and `a_bundle_keeps_its_contents_across_the_wire`. Thanks @levzzz5154!
Main's critical-hit change added the same Strike.attacker field; keep
one, then the crit particles before the victim's thorns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). I checked the loot functions against vanilla 26.3 directly. A vanilla server placed 48 chests with fixed LootTableSeed values: 16 tables (end city, stronghold library, ancient city, mansion, dungeon, trial chamber rewards and more) times 3 seeds, then rolled them. Horizon then opened the same world. Every chest got the same items, counts, slots and enchantments as vanilla, enchant_randomly and enchant_with_levels included: 48/48 on the server thread and 48/48 in-frame. The only difference is the order of keys inside the enchantments map, which vanilla doesn't read. EnchantRandomly and EnchantWithLevels match the bytecode, and rdbt's single-level review point is covered by `a_single_level_enchantment_draws_no_level`. It is also built and run in an integration tree with the other open PRs. Thanks @levzzz5154!
Turtle.finalizeSpawn sets homePos to where it spawns, and loading falls
back to the block it stands on; the home no longer moves with a turtle
that had none, which let it lay eggs anywhere. TurtleBreedGoal.breed adds
one ExperienceOrb of 1 to 7 drawn from the mother's random.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob.makeSound calls getVoicePitch before Entity.playSound checks
isSilent, so the squid's random advances either way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both random_chance terms come first in their all_of conditions, so the
level random is drawn before the block and movement checks: the boots'
4% on every location change, and the sound's 35% on every tick the
boots are worn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaMobAiOracle ran out of its 3g heap in PoiManager.tick on a busy CI
runner. Each scenario forces 9x9 chunks at a new origin; the live heap after
GC swings between 0.3 and 1 GB over a run, with peaks near 2 GB before GC.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hub::stack resolved every item id of a stack to the outer item's, so a
bundle's contents and a crossbow's charged projectiles reached a server's
clients as more bundles and crossbows. The tooltip then drew a bundle of
bundles, full. Each stack inside now looks its own id up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A remote client (single player's window on its server thread, a LAN
guest, a dedicated server's client) never ran the client side of a
piston's move: the block event reached block_entity_event, which has no
piston ticker, so moving_piston blocks arrived with no moving block
entity and pushed blocks teleported (#1704).
Vanilla's runBlockEvents broadcasts a handled event to the clients, whose
own level re-runs triggerEvent (PistonBaseBlock.moveBlocks) to build the
PistonMovingBlockEntity it then ticks and renders interpolated. Do the
same:
- run_block_events emits a BlockEntityEvent for a handled event; the LAN
and integrated hubs send it as ClientboundBlockEventPacket, before the
tick's block changes, as vanilla's broadcast precedes
ChunkHolder.broadcastChanges.
- The remote client re-runs triggerEvent for pistons, note blocks and
bells on its own level (dropResources stays server-side, as
popResource checks !isClientSide), and ticks the resulting moving
pistons each client tick so they animate and collide between the
server's states. Block events a client-side move queues run right
away, as ClientLevel.blockEvent does.
Note blocks and bell rings reach the guests through the same packet,
which they did not get before.
Co-Authored-By: Devin <[email protected]>
🤖 Agent: merged main (aec52c85e). pr_ready --quick passes (1308 horizon-client tests, clippy, 507 LOC). The welcome server mod e2e passes 35/35 in every mode: in-frame, server thread, LAN host in-frame and on its thread with a guest, dedicated with a guest. Verify is green; parity now runs on main (parity-scope). Queued.
linux-runner.sh takes its packages from apt where there is no Nix, the
registration token from RUNNER_TOKEN (a machine where gh must not be logged
in), and RUNNER_CPUS / RUNNER_MEMORY for a machine that does nothing else.
playhorizon, a partner's 8-thread server, joins parity_hosts.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker.hitByShulkerBullet tests level().getRandom().nextFloat() against
the crowding chance, not the shulker's own random.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.doSweepAttack keeps nearby entities with distanceToSqr < 9.0; it
doesn't use the entity interaction range.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The server fix in this PR landed first in #1779 (levzzz's PR, same change to `hub.rs` and `agent.rs`, with the same `lan_bot` test). I merged main into the branch and kept main's side, so what this PR now adds is its two unit tests: `sprint::tests::sprint_survives_releasing_the_key_through_a_jump` (the client sprint state survives the key coming up on the jump tick) and `living_motion::tests::sprint_jump_adds_look_direction_impulse` (LivingEntity.jumpFromGround's 0.2 impulse along the look, and none for a walk). Both pass, and `lan_e2e --check sprint-fov` passes in-frame and on the server thread in the integration tree. Labelled skip-release, because players see no change. Thanks @mc070309 for tracking the cause down and for the tests.
🤖 Agent: Merging. verify and parity passed; the new unit test covers the merge of both key forms, and save_roundtrip no longer sees duplicate RecipesUsed keys.
bf81ae635 started saving recipes_used, but Horizon's recipe ids lack the
minecraft: namespace, so a furnace could hold both forms of one id and
vanilla refused it with "Duplicate entry for key" (save_roundtrip).
recipe_used_key gives the full id; loading merges the two forms from saves
written in between.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merging. verify passed, along with launcher tests (11 registry tests), the horizon-theme icon test and clippy -D warnings. #2173, which it was stacked on, is merged.
A target block never changed its power when something hit it. Every
projectile that hits a block now asks the block how to react, as vanilla
does: a target block takes its power from where the projectile landed
(1 to 15 by the distance from the face centre) and resets after 20
ticks for an arrow or 8 for any other projectile. A player's hit also
counts the target_hit statistic.
🤖 Agent: Merging. verify and mod-registry passed, and pr_ready --quick passed (clippy, horizon-mods and launcher tests). The index's downloads field is optional, so the schema doesn't change.
🤖 Agent: Merging. verify passed and the website tests (44) pass. Ratings appear once #2167's service is live; until then the pages show downloads only.
🤖 Agent: verify and parity pass (109 harnesses affected; parity green). The dedicated-server e2e (tools/quiet_commands_e2e.py) passes 7/7. Queued. #2171 (the mod API, stacked on it) follows once this lands.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @lazy-importer!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @levzzz5154!
Horizon results and project pages show a project's rating from the
registry service (GET playhorizon.dev/api/mods/ratings, fetched with the
results, kept ten minutes): five pixel stars, the mean and how many, or
"No ratings yet". Nothing shows when the service doesn't answer.
The launcher has no GitHub account, so a project's page offers "Sign in
with GitHub to rate": it opens the site's sign-in, which comes back to the
project's page and its stars (one rating per GitHub account).
HORIZON_MOD_RATINGS (a file or https:// address) points a test registry at
its own ratings. The star icon is drawn for Horizon on the icons' grid.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
BookEditScreen's page is a MultiLineEditBox. Its getPreviousWord and
getNextWord split words at any whitespace (Character.isWhitespace, line breaks
included) and start from the cursor clamped to the last character. The
single-line EditBox.getWordPosition keeps looking at spaces only, so the line
break rule moves to the multiline box alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
Main's #1779 already keeps the sprint through PlayerInput (hub.rs, agent.rs)
and has the same lan_bot test, so this keeps main's side: what the branch adds
is the two unit tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mesher now keeps a face's light emission in its colour's alpha byte. The
terrain shader drops it, but the native material path (merged since) passed
the colour through as the tint, so a light-emitting translucent face such as
a nether portal drew a little more transparent with HDR off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
Restore integer atlas origins and sample local texel centers before nearest filtering. Cover the packed stage origins with a GPU sampling regression.
Fixes#1882.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
A bred turtle now carries an egg instead of the baby appearing at once.
She walks home, digs into sand for a while and lays a turtle egg there.
A hatching egg gives one baby turtle per egg with the egg as its home,
and a baby that grows up drops a scute.
The registry's publish job (ludovic111/horizon-mods#7) writes each
project's `downloads` into index.json, summed from GitHub's counts of its
release assets every hour. `index::Project` reads it (absent: 0, never
written when 0, so older readers and the schema are unchanged).
The launcher's Horizon results show "12.3k downloads" like Modrinth's, and
list the most downloaded first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs no longer open or break doors on their own except villagers and copper
golems. Zombies that rolled canBreakDoors break doors on hard, and
vindicators break doors on normal and hard and open them during a raid, as
BreakDoorGoal, VindicatorBreakDoorGoal and RaiderOpenDoorGoal do. A zombie's
CanBreakDoors value is now saved and loaded.
Checked against the 26.3 bytecode (tools/rea_vanilla.py):
- Cushion.interact passes (PASS) when the cushion already has a rider;
CONSUME is only for a startRiding that fails.
- CushionItem.useOn calls destroyIfInFire right after addFreshEntity, and
posts GameEvent.ENTITY_PLACE from the player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Piglins now sense the nearest block from the piglin repellents tag within 8 blocks sideways and 4 up or down (a soul campfire only while lit). When one is closer than 8 blocks and the piglin is idle with nowhere to go, it walks away from it.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
Hoglins now sense the nearest block from the hoglin repellents tag within 8 blocks sideways and 4 up or down. While one is near they become passive for 200 ticks (the attack target is dropped and no new one is picked), walk away from it, and avoid picking random walk spots within 8 blocks of it. Being hit ends the passive state.
CampfireBlock.useItemOn returns SUCCESS_SERVER (a swing) only when
placeFood took the item, and CONSUME (no swing) when all four slots are
taken.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The enchantment's location_changed effects now run for the player: the
movement_speed and movement_efficiency modifiers turn on over soul sand and
soul soil and stay on while airborne, the boots wear with a 4% chance per
block change, and walking on those blocks spawns soul particles and the
soul escape sound.
A conduit with enough prismarine around it now gives conduit power to
players in water or rain in range, hurts an enemy in water or rain when
it has 42 frame blocks, and plays its activate, deactivate and ambient
sounds, as ConduitBlockEntity.serverTick does. Its target also feeds the
client's attack particles.
A mod hands `run-quiet` a list of lines (at most 256 a call). In single
player they run as the player typing them would, with the player's source
made silent; on a Horizon server (it announces horizon:quiet_commands) they
go as one payload that the server runs the same way; on any other server
they fall back to chat commands, whose feedback shows. command.run is
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @levzzz5154!
- The list shows each project's rating (stars, mean and count) and its
downloads ("12.3k downloads"), ordered by downloads by default; an Order
menu also sorts by rating or name (?sort= too).
- A project's page shows the same, then a rating control: five stars when
signed in (the account's own rating lit), "Sign in with GitHub to rate"
(coming back to the page) when not, a note for its owners.
- Ratings come from the registry service's GET /api/mods/ratings; a rating
is POSTed with the session's CSRF token. Until the service answers, the
pages show downloads only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Right-clicking a piglin with any loved item used to consume it and the piglin threw it back. Now only piglin currency (gold ingots) is accepted from the hand, and anything else a piglin admires is worn if it can be or stored in its inventory, which saves, and drops when it dies. Piglins also pick up gold nuggets when they have room, food when they have not eaten lately, and gear that fits an empty slot.
AbstractIllager.canAttack refuses a baby villager or wandering trader.
Skip them when an illager picks a nearest target and drop them from an
illager's current target.
An open shulker hit by a shulker bullet teleports and may leave a new
shulker of its colour behind, like Shulker.hitByShulkerBullet. Levitation
from the bullet now needs the hit to have hurt the player.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
Count vexes and look for blue sheep within a 16 block radius, spare only
illagers and their vexes with evoker fangs, and let vexes ignore raiders
(including ravagers) instead of other vexes.
Gliding into a wall now hurts like LivingEntity.handleFallFlyingCollisions
(fly_into_wall, big or small fall sound). A glide no longer starts on a
climbable or under Levitation, ends there, and a climbable (not vines) ends a
glide in progress. The glide's elytra wear goes through Unbreaking, and the
glide uses the effective gravity instead of 0.08.
The first exit from the End now rolls the poem and credits, and the host
goes home once they close (PERFORM_RESPAWN); the check asserts they show.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BundleContents had no stream codec here: the host never sent what a bundle
holds, so on the server thread, LAN and a dedicated server the new tooltip
showed every bundle empty, and a vanilla server's bundle made its packet
undecodable. Its STREAM_CODEC is a list of ItemStackTemplate, the same as
ChargedProjectiles' (component 52).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`draw_item` and `draw_item_decorations` only read the game, so they take
`&self`; the hotbar loop can then hand them `self.inventory[slot].as_ref()`
and `self.offhand.as_ref()` instead of cloning the stack once per slot per
frame.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @kacpherek!
The registry service keeps players' ratings of published projects in one
SQLite file on a volume (REGISTRY_DB, /data on Railway):
- GET /api/mods/ratings: every rated project's average and count, cached a
minute; never who rated.
- GET /api/mods/{id}/rating: the signed-in account's own rating.
- POST /api/mods/{id}/rating: sets or changes it, checked like a submission
(origin, session, CSRF token), 30 per account and 600 in all per hour, only
for projects on the registry's main, never by their owners.
- The sign-in takes ?back=/mods/<id> to come back to a project's page.
The image hands the volume to nobody before running as nobody.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @nirajkhetre!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
Twice today (runs 40301239508, 40311712302) mob_ai failed in parity-main, passed when bisect ran it again at
the same HEAD, and the run still ended red, so no game release could follow main for three hours. The
harness step now continues on error and the bisect step gives the verdict: 0 when every failed harness
passes again at HEAD, 1 when they fail at the base too or a merge is named.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @vlesmentie!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
horizon-ctl's mobs answers from the server's level when the host runs on
its server thread, where the targeted player is Player, as in-frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr_ready passed (horizon-world and client tests, affected harnesses), lan_e2e passed in-frame and on the server thread. GeneratorData::load on the ThinkPad 0.64 → 0.45 s warm. Queued for merge.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
The decoded plain string replaced the component on save and on the way to
LAN guests, so a translatable name (explorer maps' filled_map.*, the
ominous banner in tests without a language) came back as its key, and
spill's ominous banner test failed. The component now stays in the patch
as loaded and is resolved in the current language when a name is drawn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: contrib-gate-17 verified this head (integration tree build, clippy, tests, parity; details in the PR comment). Re-opened from kohad1ck's PR; thanks to kohad1ck.
A drinking witch now gets the -0.25 drinking speed modifier, so it does
not walk for the 32 ticks. When it throws a potion, slowness, poison and
weakness are only picked if the target (player or mob) does not already
have that effect, as Witch.performRangedAttack does.
🤖 Agent: contrib-gate-17 verified this head (integration tree build, clippy, tests, parity; details in the PR comment). Re-opened from kohad1ck's PR; thanks to kohad1ck.
A creeper's explosion hurt mobs with no attacker, and the creeper is
already gone from the mob list when its blast is applied, so the
charged_creeper loot never matched. The creeper now announces itself
(ExplosionSource) before it explodes, its victims name it as the damage
source, and it stays known until they have been looted. One head drops
per charged explosion.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @levzzz5154!
A hurt zombie with a target now tries up to 50 spots 7 to 40 blocks away
for a copy of itself, as Zombie.hurtServer does. The copy takes the
target, and both zombies get the charge that lowers their chance.
New zombies also roll their reinforcement chance and a leader's bonus
(Zombie.handleAttributes); before this the chance stayed at 0.
Wooden buttons ignored arrows: a pressed button always released after
its delay. A button whose type allows arrows is now pressed when an
arrow (or a thrown trident) is inside its shape, and it stays pressed,
scheduling the release again, while one still is. Arrows now also run
the entityInside check on the blocks they pass through each tick.
The search only covered 8 blocks up and down. It now uses the POI index over
the whole sphere, and a bed only counts when the villager has a path to it,
as VillagerMakeLove.takeVacantBed does.
Crossbow arrows now get their pierce level from the Piercing enchantment, go through that many entities plus one without sticking, and are discarded at the next one. Arrows without Piercing behave as before. Pillager arrows now use the pierce level their weapon already carried.
A creeper is now lit by any item in the creeper_igniters tag, not only
flint and steel. A fire charge is used up and plays its own sound; flint
and steel still loses durability.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @DanielGodriNeto!
Thorns picked the nearest living mob in the world. They now hurt the mob
that caused the hit (or its projectile's owner), or the attacking player
in a player-versus-player hit.
A farmer working at its composter turns wheat into bread (three wheat for one
loaf, up to three loaves, while it holds at most 36 bread) and puts spare wheat
and beetroot seeds into the composter, emptying a full one first, as
WorkAtComposter does. Other professions are unchanged.
Mob explosions (creepers, ghast fireballs, wither skulls and the wither
spawn blast) now keep blocks when mobGriefing is off, and follow the
mob_explosion_drop_decay rule when it is on. Endermen no longer pick up
or put down blocks with mobGriefing off.
Throwing a trident now wears it through the same durability path as Riptide, so Unbreaking applies, and it still never breaks the trident. Riptide no longer launches a player who is riding something.
A thrown trident with Channeling now calls down a lightning bolt in a thunderstorm, both on a mob it hurts (if the mob can see the sky) and on a lightning rod it hits (if the rod can see the sky), with the thunder sound.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @socksprox!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @kacpherek!
A daylight detector never changed its power: nothing ran its update.
It now has a block entity with the ticker vanilla has, and updates its
power every 20 game ticks from the sky light, the sky darkening and the
sun angle. Using it to flip inverted mode recomputes the power at once.
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @kacpherek!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @some-du6e!
🤖 Agent: verified: pr_ready --quick green at this head, 33/33 affected harnesses pass locally, CI verify green, A/B shows no frame cost without a pack. A Horizon shader pack's material stage now colours terrain in the game.
A Horizon client sends a client mod's batch of command lines as one
`horizon:quiet_commands` payload, which Horizon's server announces only to
Horizon clients. Each line runs exactly as the player typing it would
(the same checks, their own permission level), with vanilla's suppressed
output: no feedback and no failure in anyone's chat. Batches wait in a
queue: at most 64 lines run per player per tick, and at most 4096 wait.
Each finished batch is one line in the server's log.
tools/quiet_commands_e2e.py checks it on the dedicated server: a non-op's
quiet setblock is refused, an op's runs, and nothing reaches the chat.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move, the first slice of splitting the client lib into crates:
- horizon-command: the command/ module (Brigadier port, vanilla tree, argument types, selectors), as is.
- horizon-client-core: what command needs from the lib, with no other dependency inside it: text, lang, snbt,
nbt_path, component, scoreboard, bossbar, gamerules, block_entity_nbt, fastutil_order; plus items moved out of
lib modules that sit in the cycle, unchanged: ParticleType (particles.rs), the key display name (keybinds.rs),
SOUND_SOURCES (settings.rs), uuid_text/uuid_from_text (save.rs), Registries/RegistryIds and parse_identifier
(command/args.rs).
horizon-client re-exports every module and item under its old path (`horizon_client::text`,
`crate::command::Registries`, `particles::ParticleType`...), so no caller changes. Inside the moved files only
paths changed (`crate::snbt` -> `horizon_client_core::snbt` in command, `crate::command::args::parse_identifier`
-> `crate::identifier::parse_identifier`...), `sign_side` became pub, lang.rs includes the lang files from
horizon-client's assets, and gamerules' test that saves a Level moved to save.rs (it needs the client's save).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A slime made without finalizeSpawn keeps 20 health where the probe's has 1,
the probe doesn't report a piglin's brain, and a NoAI bee's flower cooldown
runs one tick apart; none of that is the Peaceful despawn rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two changes so `parity.py --changed` keeps picking the right harnesses once code moves into a crate of its own:
- A harness recorded before the move names the old paths. A changed Rust file of a crate the recorded crates
link (their path dependencies, transitively) but don't name now counts, and the reach search decides.
Before, such a file matched no record and selected nothing.
- A library's root opens once reached code names the crate (`horizon_world::`, `pub use horizon_command as
command`), instead of always. Every crate root was open, so any item of a new crate whose name matched a
name used anywhere counted. On main the selection is the same for 15 of 16 sampled files; for
horizon-world/src/nbt.rs one harness that reached it only through a name match drops out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every Railway build of the registry has failed since horizon-mods gained the optional horizon-shader-api
dependency ("no matching package named horizon-shader-api"). Built the same context locally: OK.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rabbits get BreedGoal and TemptGoal with rabbit food. Damage to an animal
resets love. Horses, donkeys and llamas need canParent (not ridden, tame,
adult, full health) for both parents, a sitting wolf is no partner, and the
breeding experience orb needs the mob_drops game rule.
Gift loot (chicken eggs, sniffer seeds) is now rolled with the mob as the
loot context's this entity, and chicken variants are known to loot
predicates, so each chicken lays its own egg colour. A block a player
breaks now has the player as this entity, so snow layers drop snowballs
and chorus flowers drop themselves.
The probe's output is what the harness compares with vanilla; the codec
round trip lives in the unit tests, and its unwrap on a cost whose
components name an unmapped registry entry could abort the probe.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractHurtingProjectile.tick calls applyInertia first and then moves by
the new delta movement; Vec3.normalize leaves a near-zero vector at zero.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: game.raycast, mod API level 2. CI is green on c23b4e8e4. In game, in-frame and on the server thread, the ui guest's /raycast passes all four checks: glass at 2 blocks, nothing at 1, gold after /setblock, the sky is no hit. The ported Orbital Laser aims with it and fires in both modes. Queued.
FireBlock.getIgniteOdds returns 0 for a waterlogged state, so canBurn is
false there and the smoke takes the other branch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityType.create returns null for a type that isn't allowedInPeaceful once
the difficulty is Peaceful, so the peaceful_* scenarios threw in the oracle.
Their mobs stand for ones already in the world when the difficulty changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: harness-only change (skip-release). The old harnesses fail under server stalls, the new ones pass; 20 affected harnesses pass locally, and the two changed ones rerun after merging main.
Built on the Client of the harness-await branch (#2114). Under 2.5 s server stalls the old harness fails (a command's
feedback filed under the next step) and the new one passes 3 of 3.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: The test scripts for server mods. CI is green on d64bd7c52, and the PR is now based on main after #1833 merged. I ran pr_ready --quick on this head merged with main (b8027d4b1): fmt, clippy, the horizon-mods and welcome tests (48) and the launcher tests (132) all pass. The only affected harness is lan_integration, which runs in CI. Queued.
fireworks.rs keeps main's rocket blast damage next to the client burst;
lan_e2e.py takes main's CHECKS and server-thread default plus 'fireworks'.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Add/Edit Server, Direct Connection and Friends screens and Create New\nWorld built their titles, field labels, defaults and buttons from English\nliterals, so every non-English language showed English there. Route them\nthrough the current language with vanilla's keys:\n\n- ManageServerScreen: manageServer.add.title/edit.title, enterName, enterIp,\n selectServer.defaultName, gui.done and gui.cancel.\n- DirectJoinServerScreen: manageServer.enterIp (the addServer.enterIp key is\n not in 26.3), selectServer.select and gui.cancel.\n- WorldCreationUiState: selectWorld.newWorld.\n- Select World: selectWorld.title and selectWorld.versionUnknown.\n- DisconnectedScreen: disconnect.lost and gui.toMenu.\n\nA language missing a key keeps the English fallback, so en_us is unchanged.\n\nCo-Authored-By: ZCode Agent <[email protected]>
Menu text drawn with characters the bitmap sheets lack (Chinese, Japanese,
Korean) is rasterized into a reserve of the shared atlas at run time, and the
font caches the atlas rect it got. The loading screen hands over to the game
about two seconds after the title shows by building a new renderer, whose
atlas texture is made from HudAtlas.pixels alone: those never hold a
run-time glyph, while the cache still answers with the old rects, so every
such character already drawn went blank. Latin text, straight from the
sheets, stayed. build_renderer and loading_renderer now forget the cache
before the new texture exists, so the characters that are drawn again are
placed into it, as ui_theme clears its own cache when the atlas changes.
Fixes#1824, fixes#2040.
capture(true) now calls reset_inventory_clicks; the fixture compiles the
method body against a stub Game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: pr_ready --quick green, 30/30 affected harnesses pass locally, CI verify green. Internal: one WGSL prefixer for the shader-pack stages (no visible change).
`game.raycast(distance)` returns the first block along the player's view
(fluids pass), up to 1024 blocks: through the loaded chunks as the
crosshair's ray goes, then across the distant terrain's ground heights
(`Lod::ground`, from Stink-O's fork). The game casts it only while a running
mod imports the function. It is API level 2, so older Horizons refuse a mod
that calls it with "update Horizon".
Co-authored-by: Stink-O <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The launcher half of the mod API levels. CI is green on c8464c6bf, and the PR is now based on main after #2080 merged. Launcher tests pass, the new browser test included. The level mark is read from a real fast build of the game. Queued.
The jars an import fetches went through the proxy whatever the host, so a host
that only answers directly failed there while the launcher's own requests to it
went direct. net::agent now takes the URL and applies the same NO_PROXY and
loopback rules the launcher's own traffic gets.
Co-Authored-By: WorkBuddy AI (DeepSeek-V4.1-Flash) <[email protected]>
Dragon fireballs were a server-side list like the breath clouds, so a server's players never saw them. Each is now tracked as a minecraft:dragon_fireball; the client keeps it in the same list, flies it on as AbstractHurtingProjectile.tick does, snaps it to the server's moves and draws it with its dragon_breath trail.
🤖 Agent: Mod API levels. CI is green on b12c89650. Locally: horizon-mods lib 31, api_reference 4 and sandbox 26 pass. A dummy component that calls a function from a newer level is refused with "update Horizon". Level 1 is render.set-minimap and the players.* functions; level 0 matches 0.2.67 to 0.2.69. Queued.
The breath clouds are kept as a list, not entities, so the server thread drew their particles into its own copy and players never saw them. Each cloud is now tracked as an area_effect_cloud (radius, waiting, particle), and a server's client draws it as AreaEffectCloud.clientTick does.
Applying packs dropped the window surface and created a new one for the renderer built on the same device. On Wayland with Mesa (RADV) the new surface came back lost (ERROR_SURFACE_LOST_KHR), the new renderer failed, and restoring the old one failed too, which crashed the game.
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
GitHub started both 10-26 schedules over an hour late, so the hour check skipped both and no audit ran.
lan_integration takes ~2,400 s under load, past the 1,800 s per-harness limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: gate verified on e3e08911b (main merged). fmt, clippy -D warnings and horizon-client tests pass. lan_e2e with the server-thread host: 131/133, and cursor/falling pass on rerun in both modes. 14 in-frame checks pass. agent_control_e2e: only manage fails, and it fails the same way on main. 82 of the 83 affected harnesses pass. lan_integration flakes under load and is now named-only (nightly). Queued.
Main gained players.online, tell and broadcast (#1833) after 0.2.69, so
they take level 1 with render.set-minimap. horizon-mod-registry's own
Cargo.lock picks up wasmparser.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Track fang entities and deliver their start event to the existing client renderer. Exercise warmup, animation, authoritative positions and removal in the LAN harness.
Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
A vehicle's items are kept under a placeholder key far outside the world, and the guest container tick checked the player's reach to that key as a block, so the menu closed the tick it opened. Vanilla checks reach to the vehicle (ContainerEntity.isChestVehicleStillValid).
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
TextDisplayRenderer.submitInner calls submitTextBackground(-1, -1, width,
height), and Font.prepareBackground's effect glyph spans x0..x1, y0..y1, so the
fill is width + 1 by height + 1 from (-1, -1), as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
sounds.rs: main's Assets::load reads the launcher's files through game_files.
The rewritten-manifest test takes main's message; main's new test passes
GameFiles. ATLauncher is also looked for where the Horizon launcher looks
(~/Library/Application Support/ATLauncher, the Flatpak's data/atlauncher).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
BaseFireBlock.animateTick adds large smoke above a fire (three puffs in
its top half), or two puffs at each burnable neighbour's face when the
fire sits on nothing burnable or sturdy. SoulFireBlock.canBurn is
always true, so soul fire always takes the top-half branch. The
burnable table mirrors FireBlock.canBurn (fire.igniteOdds above zero)
from block-behaviours.json.
Mobs breaking blocks (MobEvent::BreakBlock) map to Level.destroyBlock,
whose Block.spawnDestroyParticles BaseFireBlock overrides to nothing:
a wither or another mob destroying fire no longer emits break
particles. Player breaks already route through player_destroy (1009);
the client-side predict_break particles are PR #2082's.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Level.canHaveWeather is false in the Nether and the End, so their rain and
thunder levels stay at 0. Set that once in the mob world instead of checking
the End at two of the rain sites: bees, dolphins, wolves and happy ghasts read
the same flag.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
A missing part's writes land in `dummy`; a fresh Poses started it at zero,
so a reused one must too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Send the rocket explosion event before tracked removal and render it with the synchronized firework item. Cover colored bursts and later trail frames in both hosting paths.
Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
A pack's `fn material(input: MaterialInput) -> Surface` now gives the lit
colour of every terrain surface: sections and distant tiles, solid,
cutout and translucent, in both the instanced and the flat draws. Terrain
keeps Horizon's meshes, culling and draw lists; only its pipelines change.
Their shader is the pack's stage, a prefixed copy of terrain.wgsl's
terrain section, and entries that hand the pack the texture, the vertex
tint, the light levels and the lightmap's colour apart, then apply the
alpha test and fog as `fs_solid` and `fs_translucent` do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
The game program carries its mod API level in a mark
(horizon-mod-api-level:NNNN;), which the launcher reads once per
installed version; a program from before the mark (0.2.68 and older)
serves level 0. The Horizon browser marks versions above the level of the
game the player runs (the selected version, else the newest installed)
"needs a newer Horizon than the game you play", and installs the newest
version that game can run, or says to update it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
A resource pack can rename items without changing the Search label the cache was keyed on, so a search kept its old results until the query or tab changed.
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
Sync guardian moving and active attack-target metadata, resolve the actual targeted player, and tick the client's visual charge clock. Cover charge completion and target loss for guardians and elders. Fixes#1491.
Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
Review caught three regressions against the pinned client jar: submitInner
lines stay lineHeight+1 (10 px), submitNameTag fades discrete and
see-through names with the same 0x7F, and lightCoordsWithEmission floors
both light channels at 2.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Keep hovered item stacks through draw_station and use draw_stack_tooltip, while retaining text-only station hints. Covers anvil and brewing-stand player inventory slots and stonecutter result previews.\n\nAdd an offscreen render regression which fails on the old binary for both reported screens and passes on the fix in server-thread and in-frame modes.
Co-Authored-By: Hermes Agent (OpenAI) <[email protected]>
A teleport far away (or a storage that moves by more than its size) drops
every column in one frame: 3,725 at RD 32. That frame freed every column's
sections, then removed each section from the renderer one at a time, with
a few hash lookups per section and a free-list tree update per mesh range.
- The dropped columns, and a left dimension's whole chunk map, are freed on
a short-lived thread of their own ("Chunk freeing").
- Renderer::remove_columns removes whole columns: one lookup per column for
the tables kept by column, and the meshes' ranges go back to the arena in
one sorted merge per page (FreeList::give_all) instead of a tree update
each. Per-section Renderer::remove and Occlusion::remove had no other
callers and are gone.
- The renderer's block-entity and detail-column tables use FxHashMap.
Mac mini (M4 Pro), RD 32, explored world, server thread, 3 alternated runs,
two long teleports each: the drop took 26.1-39.7 ms, now 6.9-8.5 ms.
Entering the Nether: 9.7-13.4 ms, now 4.0 ms or under 6 ms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server, arrows, snowballs, eggs, ender pearls, potions and eyes of ender only eased towards the tracker's position, which comes every 10 to 20 ticks, so they moved in jerks. Like vanilla (Entity's NO_OP InterpolationHandler) the server's position now replaces theirs at once and they fly on by themselves each tick.
A shattered eye of ender now sends level event 2003 with its item crumbs and portal rings.
Thrown potions applied gravity and drag after the move; ThrowableProjectile applies them before.
🤖 Agent: merged main (12 files conflicted with ui, render and input). The runtime now keeps the server/client Hooks split beside the ui-events export, and the Snapshot carries both the GUI fields and the server's players. Locally: the horizon-mods sandbox and unit tests, cargo-horizon-mod tests, and clippy on the client with mods all pass. CI verify and parity pass at b90ab85f4. Bottom of the server-mods stack (#1837, #1894, #1895, #1901 follow).
server_knockback, locator_bar, server_dimension_players and their one-player users now end a step by the server's
game clock (a sync round trip, N ticks, a last sync) the way server_player_targets does, so a server that stalls
under load still gets all its ticks inside the step. server_knockback's frozen section waits for the stepped tick,
the stopped clock and the landed hit; dedicated_server waits for the player's leaving before /list.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
GeneratorData::load read everything on the server thread while the chunk
workers waited: the block tables, the structure templates and the block
behaviours now load on threads of their own, and BlockBehavior::load reads
its cached-support table beside the other two. The behaviour exports
(gameplay-facts.json is 55 MB) parse from bytes in memory instead of
through a reader.
ThinkPad (2 cores), 3 alternated runs: 0.63 -> 0.45 s with the behaviour
snapshot cached, 2.40 -> 2.07 s on a first start.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EMISSIVE_LAYER took bit 14 of TerrainVertex::layer, halving the sprite
layers terrain vertices can index (32768 to 16384) and leaving no flag
bit free. The colour's alpha byte was always 0xFF: its low four bits now
hold 15 minus the light level the face emits (EMISSION_BITS), so 0xFF
still means none and the other four bits stay free. terrain.wgsl takes
only the colour's rgb and passes the level on; Java shader packs get
alpha 1; the section mesh fixture restores vanilla's 0xFF.
The HDR pass still lifts any emitting face the same way; the level is
there for scaling the lift by it later.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HDR (Graphics > Display, off by default) presents to an scRGB swapchain
where the surface offers it. The frame is drawn as in SDR, in vanilla's
gamma space; a last pass decodes it at the Paper White and lifts light
sources (blocks that emit light, lava, the sun and the moon) evenly
towards the Peak Brightness.
The mesher marks emitting faces with EMISSIVE_LAYER (bit 14 of the
layer); the frame's alpha carries the mark to the HDR pass, which lifts
only pixels that still show the world before the hand and HUD.
horizon-hdr sends HDR metadata through VK_EXT_hdr_metadata, which wgpu
doesn't: without it KWin clips scRGB content above 1.0 (80 nits).
Part of #1441.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The predicted break played the block's break sound (wool for fire) and its destroy particles. BaseFireBlock skips both (spawnDestroyByEntityParticles does nothing); players only hear the server's extinguish sound.
🤖 Agent: I looked at the launcher's Shader packs tab in dark and light (Mac mini, mktemp homes, a local registry with the sample pack): Soft Glow is listed from Horizon with Works, Page and Install, and the look matches the other tabs. pr_ready --quick is green, with the launcher's 130 tests.
The sky stage runs a copy of terrain.wgsl's sky beside the pack's code
with every name prefixed `horizon_sky_`. The prefixer moves to
`gfx/wgsl_prefix.rs` and now also renames module variables (bindings)
and overrides, so the material stage can copy the terrain section the
same way. The sky's shader is unchanged apart from where `globals` is
declared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The preset turned them off, which froze fire, water, lava and portals. Players reported fire with no animation (#2064). The option stays in Advanced Graphics for anyone who wants it off.
The harness runs the whole lan_e2e suite: ~40 minutes on a loaded runner,
and single checks flake under load (barter, riding, shulker_bullets in one
run; cursor, falling in another, all passing on rerun). Selected by
--changed it would run on most gameplay pull requests and on parity-main.
tools/parity_hosts.json gains a named_only table that every automatic
selection (--changed, --failed, the default, --flake-check) leaves out;
naming the harness or --all (the nightly audit) still runs it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The dragon's wing and head hits and its breath clouds hurt the player
without recording the dragon as the causing entity, and the dragon is
not one of the mobs note_attacker looks up. death.attack.mob and
death.attack.indirectMagic then got only the victim's name, so the
template showed through: "%1$s was killed by %2$s using magic".
Record the dragon before its damage lands (EnderDragon.hurt and
knockBack use mobAttack(dragon); its breath clouds apply
indirectMagic(cloud, dragon)).
Fixes#2062
Assisted-by: Claude Opus 5.5
Every function the game offers mods has the API level it arrived at
(crates/horizon-mods/src/api_level.rs): level 0 is the contract of
Horizon 0.2.67 and 0.2.68, level 1 adds render.set-minimap. A mod needs
the level of the newest function it imports, read from its component.
- The game refuses a mod that calls a function it doesn't have with
"made for a newer Horizon (it uses ...): update Horizon to run it"
instead of wasmtime's linking error.
- The registry index lists each mod version's level (api_level, read
from the file by CI); check_file compares it when the entry has one,
and the moved extras' installer takes the newest version this game
can run.
- cargo horizon-mod check prints the level.
- The api_reference test fails until a new WIT function has a level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: pr_ready --quick green (launcher and horizon-mods tests), CI verify and check green, merges cleanly with main. The registry takes Horizon shader packs, checked by the game's own shader check.
🤖 Agent: verified: pr_ready --quick green, 80/80 affected harnesses pass locally, CI verify green, A/B shows no frame cost without a pack. A Horizon shader pack's sky stage now draws in the game.
🤖 Agent: verify passed. Checked against the real registry now that Minimap 1.0.0 is published: the migration e2e passes 34/34 on the server thread and 34/34 in-frame. The minimap case starts at bottom left, 128, round, 0.5 blocks per pixel, turning: the mod is installed and on, its settings carried, the map shows, and the Extras page has no minimap rows. pr_ready --quick passes (clippy, 1295 horizon-client tests).
Review of #2037 (rdbtCVS): `EnchantRandomlyFunction.enchantItem` calls
`Mth.nextInt(random, minLevel, maxLevel)`, which returns `min` without
drawing when `min >= max`. The arm here drew unconditionally, so choosing
Mending (or any one-level enchantment) shifted every roll after it.
`horizon_world::loot`'s copy and `merchant.rs`'s already guard this; only the
loot dispatcher in `items.rs` did not.
Vanilla with `LegacyRandom(11)`: a Mending book followed by
`set_count` uniform 1..64 is 46; the extra draw made it 28.
`a_single_level_enchantment_draws_no_level` pins that (and the stored
Mending level).
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
Two costs a frame were repeated for every item a screen draws. ItemModels::components cloned the item's whole default component map (a JSON map of every default component) on every resolve, though most stacks carry no patch; it now borrows the defaults and copies only to apply a patch. And gui::draw submitted, culled, sorted and clipped every quad of a static item again each frame (hundreds of edge quads for a generated item, all facing sideways in the GUI), though the result for the same layers at the same position is the same; the HUD vertices of a static item are now kept per GUI position, keyed by its baked models, tints, transforms, the atlas and the exact position, and appended as they were. Items with a glint or a special model renderer are drawn as before. With the creative inventory open the HUD's share of a frame went from 0.66 ms to 0.23 ms.
draw_item_model built the client level state (dimension, game time, sun angle, moon phase, respawn) for every stack it drew, sampling the sky each time: about seventy times a frame with the creative inventory open, a fifth of the time spent on its items. Within a frame the level is the same for all of them, so it is kept for the frame (keyed by the frame, game time, day time and dimension).
With the creative inventory open, every frame cloned all the tabs with their stacks (twice), and items() cloned the selected tab's stacks again on each of its three or four calls a frame; on the search tab each call ran the whole search again, building every stack's localised name. That took the frame rate from about 1,900 to 980 with the inventory open and 340 while searching (#1953). Vanilla fills ItemPickerMenu.items when the tab or the search changes (selectTab, refreshSearchResults): the grid's stacks are now kept until the tab, the search text or the language changes, and the screen draws the tabs without copying their stacks.
In vanilla 1.20.5+, servers and datapacks customize base item names using
the `minecraft:item_name` data component rather than `minecraft:custom_name`.
Vanilla's `ItemStack.getHoverName()` checks `custom_name` first, then
falls back to `getItemName()`, which retrieves `DataComponents.ITEM_NAME`.
Previously, Horizon did not model `item_name` in `StackData`, causing items
with custom base names from servers or resource packs to fall back to
their default translation keys instead of the server-provided name.
This adds `item_name` to `StackData`, decodes and encodes it across
network item sync, and prioritizes it in `name_line` and hover tooltips.
Fixes#2039.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ctrl+Backspace on a book page with the caret after `two` in `one\ntwo` removed both lines because the word helper only knew spaces.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified: pr_ready --quick green, 38/38 affected harnesses pass locally, CI verify green, A/B shows no frame cost. Internal: a Horizon shader pack's frame uniform is shared by its stages (no visible change).
When switching sub-servers on a proxy network (Velocity/BungeeCord), the
network thread enters configuration mode and emits Event::Reconfigured.
Vanilla's ClientPacketListener.handleConfigurationStart clears the client
level, which calls Hud.onDisconnected() and resets BossHealthOverlay.
Without this reset, bossbars from previous servers remained visible on
screen.
Fixes#2034.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The official Minimap mod (mods/official/minimap) shows the extra's map
through render.set-minimap. The extra's switch and its five Extras rows
go; the game still draws a mod's map from the same tiles. A player who
had the minimap on gets the mod installed and turned on at the first
start, with the corner, size, shape, zoom and turning carried into its
storage, and one chat line. horizon-extras.txt keeps the old keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The official Minimap mod (mods/official/minimap): the minimap extra as a mod with its five options. Pixel-identical to the extra in the map region (0.000 to 0.002) with default and other settings, at the same frame cost. Verified: pr_ready (2179 workspace tests, clippy), the settings page in game, and the published mods loading beside it in both modes.
Horizon finds villager POIs from the blocks and kept no poi folder, so a
world it made had none. Vanilla rebuilds a missing section on load, but
until a chunk is loaded nothing knows its beds, job sites, bells, nests,
portals or lodestones.
Saving a chunk now writes its POI records the way PoiManager saves its
sections: each section with POI blocks, valid, one record per block of
PoiTypes with its max tickets, free_tickets left out at 0. A chunk that
lost its last POI and had a record gets an empty one, since vanilla
trusts a valid section.
The records are built on the region writer thread, which reads the
saved record there, so unloading chunks reads no files on the tick
thread. A straw bed is no home (PoiTypes.BEDS is the sixteen colours);
/locate poi uses the same table.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A generated chunk keeps the starts it holds and its save writes them
into structures.starts with their current state, as vanilla saves the
StructureStart objects its chunks share. Proto chunks get their
references and starts too, so vanilla and Paper find the structures of
the chunks they finish at the edge of a generated area.
A start with a piece that cannot be saved is left out, with a warning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A container whose items no longer match its roll is saved without the
table, also when it was loaded with one, so a chest emptied after a
load does not fill again on the next one. A zero LootTableSeed is left
out, as trySaveLootTable does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Desert and jungle temples, swamp huts, buried treasure, ocean
monuments, mineshafts, fortresses and strongholds save the tag their
StructurePiece subclass writes, with the state placement leaves: the
height a scattered feature settled on, the buried treasure chest,
placed chests, traps and spawners, and the swamp hut's witch and cat.
With these every StructurePieceType is saved.
test_structure_start_nbt_parity gets one site of each of these kinds,
and a badlands mineshaft.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Igloo, shipwreck, ocean ruin, ruined portal, nether fossil, End City
and mansion pieces save the tag TemplateStructurePiece and its
subclasses write, with the state placement leaves: the height a
shipwreck or ocean ruin settled on and the igloo's moved box. Ruined
portals save all six properties, End City pieces their genDepth tag.
test_structure_start_nbt_parity gets one site of each of these kinds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Block entity data a structure places can lack its type: a brushable
block's is only its loot table. Finished chunks get the id from the block,
saved proto chunks did not. Vanilla and Paper skip a pending block entity
without one when they finish the chunk ("Skipping block entity with
invalid type"), so suspicious sand and gravel at the edge of a
pre-generated area lost their archaeology loot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The block entity canonicaliser added spawn_data {entity: {}} to every
trial spawner that had none. Vanilla only writes spawn_data once the
spawner has picked its next mob (TrialSpawnerStateData.Packed reads it as
Optional.empty). A spawner saved while mob spawning was off, as worlds
pre-generated in peaceful or with spawner_blocks_work false are, kept the
empty entity for good: getOrCreateNextSpawnData returns it, it names no
mob, and the spawner never spawns, in Horizon and in vanilla or Paper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon kept no saveable form of its structure starts, so its saves carry
an empty structures.starts and every structure it generates is invisible
to vanilla and Paper (locate, structure spawns, location predicates,
advancements) and cannot be continued past the generated area.
StructureStart::save and Piece::save write the tag vanilla writes. This
covers the pieces of jigsaw pools (villages, pillager outposts, ancient
cities, trail ruins, trial chambers, abandoned camps, bastions) with
their pool element, junctions and liquid settings. Junctions keep
delta_y and dest_proj, pool elements their processors as written.
LegacyPiece::save defaults to None; a start with such a piece is not
saved, as before.
test_structure_start_nbt_parity compares the tags with the pinned
server's own for one site of every jigsaw structure kind.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chunk holds the Overworld's 24 sections in every dimension, and the save
wrote them as they are: yPos -4, sections -4 to 19 and heightmaps counted
from y=-64. Vanilla and Paper trust those heightmaps, so a Horizon-made
Nether reads its surface at y=192, above the bedrock roof, and the End 64
blocks too high. Re-saving a vanilla Nether chunk rewrote its yPos too.
LevelShape (skylight, min_y, height per dimension folder) replaces the
save path's sky_light flag. yPos, the saved sections, the PostProcessing
lists and the heightmaps follow it, as SerializableChunkData.write does.
LAN clients in the Nether and End now get the chunk's heightmaps; the
heightmap check failed there and they were sent empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Everything the launcher fetches now goes through one HTTP proxy when there is
one: Horizon's releases and the launcher's own update, Minecraft's files and
Java from Mojang, Modrinth, CurseForge, the mod registry, pictures, Microsoft
sign-in, Tailscale's API, and the jars a modpack import downloads. It is
Settings' Proxy row (`proxy` in settings.json), or `horizon-launcher settings
proxy http://host:port`; without one, HTTPS_PROXY, HTTP_PROXY and ALL_PROXY
apply, with NO_PROXY naming the hosts to reach directly.
A local address never goes through it, and the Servers page's calls to a game
server's API stay direct: they are on loopback and asked every second.
horizon-port-pack builds its own agents, so the launcher hands it the address
(net.rs); run on its own, it reads the same variables.
Co-Authored-By: WorkBuddy AI (DeepSeek-V4.1-Flash) <[email protected]>
Hovering a bundle showed only its name and `BundleItem.appendHoverText`'s
"Can hold a mixed stack of items": nothing drew the contents, so a player
could not see what was inside or how full it was (#2001).
`ClientBundleTooltip` is ported to `bundle::tooltip_layout`: the 96 pixel
column, the four-wide grid filled bottom-up and right-to-left from
`getNumberOfItemsToShow` (eleven stacks and a `+N` when it overflows), the
progress bar at `Mth.mulAndTruncate(weight, 94)` with the full sprite and the
`item.minecraft.bundle.empty`/`.full` labels, and an empty bundle's wrapped
description. The game draws it under the stack's own tooltip
(`Game::draw_stack_tooltip`), which the inventory, creative, container,
beacon and trading hovers now use.
Verified in game: a bundle with 64 stone shows one slot and a full red bar
labelled "Full!"; one with 32 stone and 8 dirt shows two slots (32 stone
first, as the next out) over a half-filled blue bar; an empty one shows the
description and "Empty".
🤖 Agent: Mod API render.set-minimap: a mod picks the corner, size, scale, shape and turning, and the game draws the map from the minimap extra's tiles. It is a new function, so published mods keep loading. Verified: pr_ready (1353 tests, clippy, launcher), the sandbox test, and in game beside the published mods in both modes.
🤖 Agent: Zoom moved to the mod browser. The built-in zoom is removed, and a player who had it on gets the published Zoom mod with their settings and key. Verified against the real registry, in both modes (54/54), plus clippy and the client tests.
A server's chunk columns (single player on its server thread, LAN,
servers) were decoded on the render thread: sections, palettes, biomes
and light, about 0.1 ms each on an M4 Pro. Once the backlog was overdue,
a frame decoded every waiting column at once. Now each chunk packet goes
to a decoder thread as soon as it arrives, and the frame that reaches it
in order only inserts the column. A column still decoding holds back what
follows it, overdue or not, so a frame never waits for the decoder. A
dimension or registry change since a column was decoded makes the frame
decode it again with the new format.
Forgetting a column removed its pending sections with a pass over all of
them. A moved or shrunk storage forgets thousands of columns in one frame,
so it now removes them by key and drops their block entities in one pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: main merged into the branch (merge commit). fmt, clippy -D warnings and the horizon-client tests pass; the 30 harnesses parity.py --changed picks for the game-binary files pass (30/30 on an integration of #1896#1809#1812); verify passed on this head. The earlier parity failure was the stale-head runner setup (.local/horizon-client link).
🤖 Agent: main merged into the branch (merge commit). fmt, clippy -D warnings and the horizon-client tests pass; the 30 harnesses parity.py --changed picks for the game-binary files pass (30/30 on an integration of #1896#1809#1812); verify passed on this head. The earlier parity failure was the stale-head runner setup (.local/horizon-client link).
🤖 Agent: main merged into the branch (merge commit). fmt, clippy -D warnings and the horizon-client tests pass; the 30 harnesses parity.py --changed picks for the game-binary files pass (30/30 on an integration of #1896#1809#1812); verify passed on this head. The earlier parity failure was the stale-head runner setup (.local/horizon-client link).
`AbstractContainerScreen` keeps `isQuickCrafting` and the drag it belongs
to on the screen. Horizon keeps both on `Game` (`inv_clicks`, `inv_drag`,
`last_inv_click`), and nothing cleared them when a screen closed: a press
whose release never reached the screen — the pointer left the window, or the
screen closed between the press and the release — left a quick-craft drag
running, and `Clicks::press` answers `None` for every later press, so clicks
went dead on every screen after it.
`Game::reset_inventory_clicks` now runs when a container closes and when the
pointer is grabbed again (the inventory's screens are behind us), which is the
lifetime vanilla gives that state.
Refs #1737
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
On a server's client (single player on its server thread, LAN guests,
dedicated servers) the dragon was tracked but never built: spawn_entity
only makes mobs from mob_types(), which has no dragon, and
dragon_geometry draws Game::dragon. The crystals had their own remote
constructor; the dragon had none.
The client now keeps the server's dragon in Game:🐉
handleAddEntity builds it (full health, hovering, as its constructor
does), SetEntityData applies DATA_HEALTH_ID and DATA_PHASE,
SetEntityMotion its velocity, and its moves interpolate like other
living entities. tick_remote_dragon is the client side of
EnderDragon.aiStep: wing flaps and their sound, the growl,
checkCrystals' nearest-crystal search for the healing beam, the flight
history and the parts; a dead dragon rises and bursts as in tickDeath.
Hurt animations flash it, removal and level changes drop it, and the
player's attacks pick its parts and send the server their ids.
The in-frame fight is unchanged; its nearest-crystal search moved into
nearest_crystal_to, shared with the client.
Fixes#1530.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's `sky` function gets vanilla's colour along each view ray and draws
where the vanilla sky dome draws, under the sun, moon, stars and clouds. The
pipeline's shader is the pack's checked stage (`CheckedStage::source`, new)
followed by a copy of terrain.wgsl's sky with every name prefixed
`horizon_sky_`, so none can meet a pack's; terrain.wgsl's `fs_sky` now calls
`sky_along_ray`, the same code as before.
A running pack is one `NativePack`: the frame every stage reads (written once
a frame), its sky and post stages and its pass timer. In a frame F3 times, the
sky draws in a pass of its own so the overlay can show its time; otherwise it
stays in the world's pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`NativePack` holds what a running pack needs: the `frame` every stage
reads (`NativeFrame`, written once a frame before the world draws), its
post passes and the timer of its passes. `NativePost` no longer owns the
frame buffer or the timer, so the sky stage can read the same frame and
be timed by the same timer. No change in what draws.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged main in. The old head's parity failure was server_dimension_players, which passes on main's parity-main. tests/test_parity_reach.py OK.
The minimap extra as a mod (mods/official/minimap): the extra's five
options on its settings page, kept in its storage with the extra's values
(corner, size, shape, zoom, rotate), and the map drawn by the game
through render.set-minimap, so it looks and costs the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: pr_ready --quick green, 32/32 affected harnesses pass locally, CI verify green, paired A/B shows no frame cost without a pack. F3 now shows a shader pack's GPU time per pass, for both pack kinds.
Send each sonic ray point through the existing particle packet path with vanilla recipient filtering. Cover nearby and distant clients, damage, and local rendering in the LAN harness. Fixes#1490.
Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
Add cushion entities, shape-based placement and support checks, colored rendering, server tracking, sitting, destruction and save/load support. Compare placement rules against vanilla and exercise host/guest interactions.
Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
What the official Minimap mod needs: a mod picks the corner, side, scale,
shape and turning, and the game draws the map as the minimap extra does,
from the same tiles (vanilla's map colours, the floor under roofs) within
its own frame budget. One map at a time, the first loaded mod's. A new
function and type, so mods built before it keep loading.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Run the client branch of Squid.aiStep, preserve authoritative movement and synchronize tentacle-cycle resets with entity event 19.
Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
Check the tool component before server destruction, local mining and remote prediction. Cover vanilla item defaults, overrides and mode-switch cancellation.
Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
check still found five Help wanted items whose only links were closed
issues. Each was finished by a merged pull request, so the item now links
that pull request and waits in Now until a release carries it.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
ludovics-mac-mini-2 holds only the horizon-parity-main label; the release
runner no longer has it, since a release waited behind each new
parity-main run there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: cargo horizon-mod test runs a mod's test scripts with no game. Main is merged in, and the runner fills the new snapshot fields (input, render, frame stats) with the trial run's screen size. Verified: clippy, the cargo-horizon-mod, coords-hud and horizon-mods tests (57), launcher, and cargo horizon-mod test on coords-hud (2 scripts).
🤖 Agent: verified: parity for the 75 harnesses parity.py picks passes locally, pr_ready --quick passes on the final head, lan_e2e mobs/hazards pass in-frame and on the server thread. Same results; −1.3% of the dedicated server's tick-thread cycles at 10 bots.
🤖 Agent: The official Zoom mod (mods/official/zoom): the zoom extra as a mod, pixel-identical and at the same frame cost. Verified with main merged: clippy, cargo horizon-mod check, and in game in both modes. Publishing follows in horizon-mods.
TooltipDisplay.shows built a JavaString from the id on every call just to
test membership in the NBT list. A hovered tooltip asks this a dozen times
per frame, so the hover allocated a dozen short vectors per frame for a read.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
🤖 Agent: The FPS counter and Keystrokes moved to the mod browser. A player who had one on gets the published official mod installed on the first start, with their settings and a chat line. Verified against the real registry, in both modes (34/34), plus clippy and the client tests.
update rebuilt the values vector every frame for a fixed-length list: one
allocation and one free per frame for nothing. eval_custom already treats
the same vector as a buffer to truncate and extend.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
CampfireBlockEntity.cookTick and cooldownTick call setChanged whenever
a timer moves (cookTick while any slot holds food, cooldownTick while
any progress is above zero). tick_campfire only marked the chunk when
an item finished, so after a save partway through cooking, an unload
saved nothing and the progress since went back to the saved value.
The chunk is now marked modified on those ticks, with no update to the
clients, who don't see the timers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Mods page gets a fourth kind, Shader packs, which searches Horizon's
registry and installs a `.hshader` into `shaderpacks/` once
`index::check_file` has run the game's own `horizon_shader_api::check`.
Modrinth and CurseForge answer that tab with a pointer to the registry:
their shader packs are the shaders/ format and are not offered.
The sample index lists a shader pack (`glow`, made with
`horizon-shader new` and `pack`), so the registry build, check_file and
the launcher's install are tested on a real file. A file of the same
name the registry didn't put in place is no longer called a mod.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: horizon-ctl options-set no longer turns every mod off: Settings::with_option keeps the mod list with Horizon's other files. Verified: unit test, clippy, client tests, and in game in both modes (10/10).
A mob's fluid state and its supporting block each read every cell around
its box, every tick. World::may_hold answers from the palettes of the
sections the box reaches: with no fluid listed there the fluid scan stops
before reading a cell, and with no block that reaches out of its cell
(large collision shapes, moving pistons) the supporting block search
reads only the cells meeting its area, as BlockCollisions' cursor does.
Results are the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
options-set rebuilt Settings from options.txt and carried over Horizon's
graphics, controller and extras files, but not the mod list: every mod
read as off afterwards, and the next save wrote an empty
horizon-mods.txt. Settings::with_option now does the rebuild and keeps
all four.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The overlay measured every line to place it and measured it again for its
background box, about thirty extra glyph walks per frame on a full screen.
The width that placed the line is carried to the background pass.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The max-width pass built a Text for every audible subtitle and dropped the
width it measured, then the draw loop built the same Text again to measure
it a second time. The row loop uses the number the first pass already had.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
first_line collected the whole line into a Vec<char> and then re-collected
a prefix of it, eight times per sign per frame, front and back. Every cut
point it needs is already a byte offset in the input.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
🤖 Agent: Players rebind a mod's keys on its Options > Mods page (the game's own key rows, saved in horizon-mods.txt), with no WIT type change, so the published mods keep loading. Verified with main merged: clippy, the client, mods and SDK tests, and the in-game check in both modes (24/24).
The page lists the packs in shaderpacks/: Horizon's own, marked (Horizon) and
named by their manifest, then packs in the shaders/ format. One runs at a
time: clicking a pack picks it, clicking the picked one turns it off, and
Open Pack Folder opens the folder. The pick is saved in horizon-graphics.txt
under a new key (pickedShaderPack), so the shaderPack value older versions
saved stays ignored. A new pick drops the running pack and loads the new one
in the next frame; HORIZON_SHADER_PACK still wins, for development.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While the F3 overlay is open, the running pack's passes are timed on the GPU,
a shaders/ pack's or a native pack's, and the overlay lists the pack's total
and its costliest passes (the median of the last 15 timed frames). The timer
never waits: a timed frame's stamps are resolved once the GPU has finished it
and read back once mapped, then the next frame is timed. A native pack's first
pass counts from a mark that ends with the world's draws, so it does not carry
the time it waited for them. HORIZON_SHADER_PACK_TIMING still logs the medians.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: native shader packs' post stage in the game with hot reload; pr_ready, 66 affected harnesses and a paired A/B (no cost without a pack) passed.
The official Zoom mod (mods/official/zoom) does what the extra did. The
extra's zoom, its key and wheel hooks and its Extras rows go; the camera
keeps a mod's zoom (render.set-zoom). A player who had zoom on gets the
mod installed and turned on at the first start, with the starting zoom,
smooth zoom and slower turning carried into its storage and a changed
key into horizon-mods.txt, and one chat line. horizon-extras.txt keeps
the old keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The early return skipped the child loop as well, while skipDraw only hides
the part's own cubes. A rolled armadillo skips its body and still renders
the parts below it.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
`/loot give @s loot minecraft:chests/end_city_treasure` handed out bare gear: the
loot dispatcher in `items.rs` (the path `/loot`, bartering, raids, dispenser
tables, advancements and mob drops use) knew no `minecraft:enchant_with_levels`
or `minecraft:enchant_randomly`, so both fell through its arm untouched. Only the
chunk-generation path, which uses `horizon_world::loot`, enchanted anything —
which is why #1911 and #1437 looked fixed.
The dispatcher now runs both, as vanilla does: `EnchantWithLevelsFunction` picks
from `EnchantmentHelper.enchantItem`'s results (a book becomes one enchanted
book), `EnchantRandomlyFunction` picks one compatible enchantment at a level
between its min and max, and both resolve `options` as a tag, a list or one id,
else the whole registry. `Items` carries that registry now
(`Items::enchantments`), and `StationData` shares the same copy instead of
loading a second one.
Verified in game: six `/loot give … end_city_treasure` rolls gave 10 pieces of
gear, all 10 enchanted (unbreaking 3, mending 1 + protection 4, …), where all 10
were bare before.
Refs #1660
The bisect ended on the newest merge when the failed harnesses pass
everywhere now (a flake, or a runner that was missing the sounds), and
on the oldest when they fail everywhere (the runner), and commented on
that pull request either way. It now reruns them at HEAD and at the base
first and names nobody unless HEAD fails and the base passes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of #1932 (rdbtCVS): with `--in-frame`, opening a world to LAN and
setting Commands Only still showed a guest's ordinary chat on the host. The
hub's `HostEvent::Chat` went through `system_message`, which asks for
`receive_system_messages` — true at Commands Only — so the line passed.
`Game::player_message` shows a chat line as `ChatSource::Player`, which is
the permission `ChatListener` checks for player chat; `lan_log_event`
routes `HostEvent::Chat` there. `/say` and `/me` arrive as chat messages
too (say_command, emote_command), so they follow the same permission as they
do for the guests' own packets; command feedback such as `/list` is a system
message and keeps the system-message permission.
Reproduced on the pre-fix build: host at Commands Only, guest sends
`baseline-visible` → the host shows `<Guest9> baseline-visible`. With this
change the same line stays hidden at Commands Only and shows at Shown.
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
🤖 Agent: Horizon's official FPS Counter and Keystrokes as mods in mods/official/, same options and look as the extras (pixel-identical, frame cost within noise). Main merged in (wasmtime 48). Verified: pr_ready, clippy and tests of both mods, horizon-mods tests, both mods' settings pages in game.
Review of #1933 (rdbtCVS): `Game::tick` returns through `tick_remote` whenever
`remote` is set — every multiplayer session and the default single player — so
the `tick_world` call that advanced the entries never ran there. A thrown
pearl's slot stayed at full shade until the server's duration-0 packet arrived
instead of fading over the second.
`tick_remote_level` advances them now (Player.tick's ItemCooldowns.tick); the
groups that end are dropped silently, as the client's plain ItemCooldowns
does.
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
A native pack named by HORIZON_SHADER_PACK (a folder with horizon-shaders.toml
or a .hshader in shaderpacks/) is checked, then its post passes run over the
world's own target, the one a shaders/ pack reads: no second target and no
extra pass, and without a pack the frame only tests one Option. The pack is
looked at twice a second; a saved file is checked and rebuilt in place, and a
version that fails is refused in the chat while the last good one keeps
drawing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new case in ui's widget variant stops every ui mod built before it from
loading: the runtime links a component only when each imported
function's types match exactly ("expected variant of 5 cases, found
4"), so the published FPS Counter and Keystrokes would break. Instead,
the game lists a mod's keys first on its page in Options > Mods, as Key
Binds rows, and a mod with keys gets a page even without ui. No WIT
type changes. docs/api/stability.md said a new case the mod sends isn't
breaking; it is, and now says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
macOS temp dirs live under /var, a symlink to /private/var, so the
resolved report path never equalled the unresolved fallback.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apply the vanilla Mob.checkDespawn Peaceful gate before persistence and player distance checks. Extend the mob AI parity harness with difficulty, persistence, name-tag and NoAI regressions and allowed-type controls.
Fixes#1942
Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
🤖 Agent: Mod API (experimental): render.set-zoom and input.claim-wheel, held mod keys let go when a screen opens, mod keys on mouse buttons. The consume choice is in stability.md. Verified: pr_ready (1300 tests), sandbox tests, in game 22/22 in-frame and on the server thread.
Keeps both new block entities: BlockEntity::Campfire (this branch) and
BlockEntity::Skull (main), in containers.rs's enum, id() and the menu
lists in game.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The release runner, now parity-main's, cloned the shared source before
the sounds were added to it; vanilla-data.sh only copied on the first
run, so ambient_sound, audio_attributes and music failed there with
"Sound assets missing". Each run now clones any entry the runner's copy
lacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mods/official/zoom: hold the key (Z) to zoom in at the starting zoom,
the wheel changes it while held (x1.25 a step, 1.25 to 10), smooth
easing and slower turning as options. Same settings page as the extra,
with the key widget for the key. Built on render.set-zoom,
input.claim-wheel and ui's key widget.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Thanks socksprox! Mannequins render as the player model with their profile skin. I pushed d3af1e04e: a mannequin is a LivingEntity, not a Mob, so it runs no goals and never despawns. Checked in game: a summoned mannequin stays put for 400 ticks on the server thread and falls to the ground in-frame (it wandered 14 blocks before). Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
🤖 Agent: Thanks levzzz5154! Matches ItemCooldowns and the slot sweep in Gui.renderSlot; thrown, eat and shield pass on both hosts. I merged main and dropped orphaned doc comments. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
🤖 Agent: Thanks levzzz5154! Matches ChatComponent's ChatVisiblity filter for the Full, Commands only and Hidden options. I pushed 1e6f05ed5 earlier: every line keeps its source, so changing the option refilters the history both ways (new test). Verify had failed because main's mod output (#1875) calls Chat::add without a source. I merged main, and 2256941af marks a mod's lines as the client's own, which the Chat option keeps. Clippy and the chat tests pass after the merge. Before it, the wave 2 batch passed its 127 affected harnesses, and lan_e2e chat passed with the host in-frame and on its server thread.
🤖 Agent: Thanks rdbt! Every server player now emits its own movement game events, with per-player sculk history; the new server_player_sculk harness passes. I merged main (the guest swap code). Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
🤖 Agent: Thanks vlesmentie! Matches vanilla 26.3's AbstractWidget.handleCursor: buttons show the pointing hand. lan_e2e's cursor check passes. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
🤖 Agent: Mod API calls the official HUD mods use: render corner panels, text width and frame stats; input mappings and clicks per second; a ui button widget. Main merged in (after #1914's widget refactor). Verified: horizon-mods tests, clippy with and without mods, horizon-client tests (1121 lib), Keystrokes' settings page in game.
🤖 Agent: Thanks rdbt! Matches the client's LargeFireball tick between tracker updates; the new remote_fireball harness passes and lan_e2e's fireball check sees it on both sides. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
A ui page's key widget shows one of the mod's keys as Key Binds does
("Zoom: Z"). A click waits for the next key or mouse button, which
becomes the key; Esc binds none. The game keeps the choice in
horizon-mods.txt (key.<mod>.<name>:key.keyboard.z), applies it when
the mods load and to keys registered later, and lets a held key go when
it is rebound. register-key's key is now the default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Thanks vlesmentie! Matches BigDripleafBlock.entityInside and canEntityTilt (the 0.6875 height check): a big dripleaf tilts under a player standing on it. Tested with the wave 2 batch: fmt, clippy and the horizon-client and horizon-world tests pass, and all 127 affected harnesses pass. The chunk_generation and decorated_region reruns took 8 minutes with no chunk differing. lan_e2e contacts and the rest of wave 2's checks pass with the host in-frame and on its server thread.
🤖 Agent: ready. server_ops_feedback waits for vanilla's and Horizon's 'Saved the game' before the save step's 2 s, so a loaded runner (the desktop's full trial) no longer cuts the transcript. Harness only. verify passed; parity ran server_ops_feedback on the Mac: PASS 120 s.
🤖 Agent: requeued after a runner-setup parity failure (subtitle: missing sounds.json on the runner); 30/31 harnesses passed, the 56 selected passed locally. Queued by perf-next-2.
🤖 Agent: Thanks vlesmentie! Matches LayeredCauldronBlock.entityInside / handleEntityOnFireInside: a burning player is put out and the cauldron loses a level. The cauldron harness passes, and lava and contacts pass on both hosts. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
🤖 Agent: verified: the fix for #1772 plus #1947 merged in. Its parity job prepares with this preparer and checks that the output is still OUTPUT_VERSION 1, so with #1946 players keep their prepared resources. Merging.
Horizon had no CampfireBlockEntity, so right-clicking a campfire with raw
food did nothing. Now:
- BlockEntity::Campfire keeps four slots with their cookingProgress and
cookingTime, saved as Items, CookingTimes and CookingTotalTimes; both
campfires share the minecraft:campfire block entity type. It is not a
Container (only Clearable), so hoppers don't reach in.
- CampfireBlock.useItemOn: a campfire_cooking input goes into the first
free slot (placeFood) with its recipe's time; the use is consumed
whether or not there was room, the item is used up outside creative,
interact_with_campfire is awarded and a block_change goes out. A client
of a server leaves it to the server.
- cookTick (lit): each item's progress rises; a finished item's result
drops at the block (Containers.dropItemStack), the slot empties and a
block_change goes out. cooldownTick (unlit): progress falls by 2.
- Breaking the campfire drops its items (preRemoveSideEffects).
- The update tag (Items) reaches a server's clients, and CampfireRenderer
draws the items on the fire.
Fixes#1988.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's mod output calls Chat::add without the new source; a mod's line is
written by this client, so the Chat option keeps it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Thanks socksprox! Player heads now show their owner's skin, fetched like vanilla's SkullBlockRenderer from the profile's textures property; TEXTURE_URIS limits it to textures.minecraft.net, so there's no new host.
🤖 Agent: Thanks vlesmentie! Matches Chicken.aiStep's flap on the client of a server. I merged main and slimmed the test to reuse an existing mob kind. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
SkullBlockRenderer's SKIN_BY_TYPE is DefaultPlayerSkin.getDefaultTexture(),
DEFAULT_SKINS[6] = entity/player/slim/steve; the block_entity_render harness
found Horizon drawing nothing for heads without a profile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The profiling marks that landed in main (mark_entity, mark_stage) collide with
the cached cracks scan; both are kept.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Both branches changed how the harness finds the mod jars: resolve every jar
through jar_path (jars, then pack-jars) and keep the profile packs check.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: Mod API capability input (experimental): claimed items' use and the mod's keys. Main merged in beside command and render (9 capabilities). Verified: horizon-mods tests (sandbox, API reference), clippy with and without mods, horizon-client tests (1121 lib), earlier pr_ready and in-game runs in both modes.
It records an agent's runs and diagnoses, which the pull request and its
branch history already keep; docs/ is for how the project works.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verify passed at 0c84eb307. Merged by hand: the PR parity job is queued behind the same runners, and this changes only how that job prepares its data. The next PR parity job on ludovics-mac-mini is the real test.
🤖 Agent: pr_ready and 91 harnesses pass (shared run, body, incl. decorated_region's stronghold site and world_spawn); first chunk 8.7 → 6.0 s on 4 threads. Queued by perf-next-2.
🤖 Agent: Thanks levzzz5154! The Game Rules and Statistics scroll bars can be pressed, dragged and released like vanilla's; I merged main and pushed a41381236 (Statistics uses the GUI height; #1913's drag test is folded in). Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
Villager.setVillagerData sets offers to null when the profession changes,
and getOffers rolls the new profession's trades on the next look.
set_profession cleared the offers but left unset false, so ensure_offers
never rolled new ones: a villager that took a placed workstation after
losing its first wore the new profession but only shook its head.
It now marks the offers unset, as vanilla's null.
Fixes#1998.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both branches changed the machine screens' title drawing: keep the styled server
title from this branch and the translated container title and inventory label
from feat/server-dialog-screen.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verify passed at 2c612fd79. The PR parity job is still queued behind the same runners, and it can't test a runs-on change, so I merge by hand.
A PR head older than the fix in vanilla-data.sh left a real
.local/horizon-client without sounds.json on runner ludovics-mac-mini;
every later job there failed subtitle. Like the harness selection, the
job now runs main's script unless the branch changes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zero passenger walk position and speed like vanilla LivingEntityRenderer, covering local and remote riders. Extend entity model parity with moving passenger and on-foot render-state cases.
Fixes#1962
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
🤖 Agent: Thanks vlesmentie! Structure chests keep the enchantments their loot table rolled. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
The queued parity-main run waited 2 h behind PR parity jobs on the two
horizon-game runners, each merge replacing the pending one, and releases
need it green. The release runner gets a horizon-parity-main label; a
release only starts after parity-main passed, so the two never compete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
render.set-zoom divides the field of view by up to 10, eased through the
game's FOV easing or at once, with a turning scale (at least 0.05); only
while playing, and the spyglass's own view wins. The strongest of several
mods' zooms counts, and the built-in zoom keeps working beside it.
input.claim-wheel takes the wheel from the hotbar while playing: its steps
are the mod's wheel events. That is the one input a mod consumes; its keys
never do (docs/api/stability.md). A screen opening now lets the mod's held
keys up, and mod keys bound to mouse buttons (key.mouse.4) are heard.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Capability::ALL is 9 with Input; the SDK re-exports command and input;
the API reference is regenerated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Thanks levzzz5154! End city banners keep their template patterns; I pushed 401ac1322 so the structure code and the save loader share one banner decoder. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: Thanks cristibctr! Windows pack cache recovery; it bumps the cache version, so every platform rebuilds that cache once. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: verified: the launcher keeps prepared resources while the preparer's OUTPUT_VERSION (or a legacy fingerprint for v0.2.49 to v0.2.64) is the same. Unit tests and clippy pass. End to end with the real v0.2.64 release: an update to a build with OUTPUT_VERSION 1 reused its resources, and a build with the number bumped to 2 did not. Merging.
Queue successful mob and player crit animations until the hub can send them to the attacker and tracking clients. Cover falling, grounded and refused attacks in both host modes. Fixes#1898.
Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
🤖 Agent: Thanks rdbt! Matches ItemFrameRenderer: a targeted frame shows its item's custom name. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: ready. Linux runner support (runner-env/vanilla-data Linux paths, linux-runner.sh, runner_pause.py with Ludovic's lock/idle-15-min rule, heat, game, GPU and pause-file checks), desktop parity host, nightly and parity-main pinned to the Mac. verify and parity passed at this head. The desktop runner stays without the horizon-game label until its trial is identical: that is a runner setting, not in this PR. Desktop full run: 342/343 in 39.3 min (the 1 is a harness timing flake, fixed by #1997).
🤖 Agent: Thanks Domanffe! Enchantment names and levels follow the selected language; the server thread shares the client's language, and with none loaded (dedicated server, probes) the English names stay. I pushed a doc tweak (b7629cf65). Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: Thanks socksprox! Refactor of the mod screen widgets plus a mouse recapture when a mod's screen goes away. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: Thanks socksprox! Particles keep their sprites across a resource reload (content root set again on reload). Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: Thanks rdbt! Matches AnvilMenu, which sends the level event with a null player so every player hears it once; lan_e2e's anvil check heard it exactly once on the host and once on the guest. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
A mannequin is an Avatar (a LivingEntity), not a Mob: its serverAiStep has
no goals and LivingEntity has no checkDespawn. With the passive brain a
summoned mannequin wandered off (14 blocks in 400 ticks on the server
thread) and could despawn far from players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Thanks rdbt! Matches Leashable.tickLeash / dropLeash: a leashed mob that dies drops its lead; the new mob_ai scenarios run against the vanilla oracle. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: Thanks rdbt! Matches ServerPlayerGameMode.handleBlockBreakAction: an instant survival START never leaves a block being destroyed, so no repeated hit sounds and a replanted crop isn't broken again. It now includes #1966's sound regression. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: Thanks sl4ppleware! Matches LocalPlayer.hurtTo: the hearts blink again when hurt, and the remote client ticks the blink through its controlled simulation tick. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: merged after verify (bugs-dezerx-1's fix for a player's bug list, self-checked with pr_ready); harnesses run on main by parity-main, which gates releases.
🤖 Agent: Thanks rdbt! Matches vanilla: the saved GUI scale isn't clamped while a temporary small surface is up, the HUD caps the scale it draws with. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
On the desktop runner (8 harnesses at a time) vanilla's save-all took 2 s,
the whole step: the transcript ended before "Saved the game" and the
harness failed with Horizon right. The save step now waits for that console
line, on both sides, before its usual 2 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Thanks rdbt! Android-only cfg change to helper thread priorities; the desktop targets compile the same code as before. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
🤖 Agent: merged on verify: tooling only, it changes how fast harnesses are chosen, not which game code runs. Fixes pr_ready's 14+ min planning (affected_harnesses was called once per harness) and bounds the selection.
🤖 Agent: Thanks rdbt! Checked against vanilla's Inventory order (armour FEET to HEAD, then the off hand) when items are cleared. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
- The reach parse cache lives in the shared parity folder, keyed by content,
so a new worktree starts warm (written atomically, other worktrees'
entries kept within bounds).
- A harness's reach search stops as soon as it reaches a changed file that
differs in more than its `mod` declarations; partial searches are not
memoized. Declaration diffs are memoized (one git show per file).
- A harness whose changed dependency is outside crates/ is picked without a
search (narrowing always keeps those).
- After PARITY_REACH_BUDGET seconds (default 180) the remaining harnesses
count every source their probes link.
- crate_of is cached; no pathlib in the hot paths.
Same 79 harnesses on the 19-file diff from contrib-gate-16; CPU 19.9 s cold /
16.6 s warm before, 13.1 s cold / 9.9 s warm after (Mac mini under load).
parity.yml: skip the prepare_output check for heads made before #1947 (no
OUTPUT_VERSION, no tool): the job runs main's workflow on the head's files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: ready. A failed horizon-prepare now leaves nothing in .local, and stale real .local/horizon-client* folders and preparations lacking horizon-client are cleaned at job start. Tested on a fake checkout (fail, stale, cached). verify passed; parity skipped (scope). No overlap with #1965.
🤖 Agent: Mod API capability render (experimental): HUD shapes and camera shake. Main merged in beside command (8 capabilities). Verified: horizon-mods tests, clippy with and without mods, earlier pr_ready, harnesses and in-game runs.
The usage line, the docs and the website list `test` with main's
`new --template code|content`; the code template keeps tests/hello.toml.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both add a Capability variant: the list holds 8 now. The SDK re-exports
both interfaces; the API reference is regenerated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: thank you kacpherek! /pregen is in, off by default: a server turns it on with horizon-pregen=true in server.properties (server.toml [properties]), single player and LAN with --pregen. With it off, the command tree and /help match vanilla. Tested on a dedicated server through horizon-server (on: 1,369 chunks generated and saved), single player on the server thread and in-frame, and 11 harnesses including command_exec.
When horizon-prepare failed, vanilla-data.sh deleted the preparation folder but kept what it had
already written to .local/ (horizon-client, horizon-client-motion, ...). The next job then saw those
real folders as already there: its preparation listed only horizon-client-structures in .entries,
and the checkout kept using the stale resources outside the per-key cache.
Now a failed preparation removes the .local/ entries it created; a job first removes real folders
named like preparation outputs, and makes again a preparation whose .entries lacks horizon-client.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify (bugs-dezerx-1's fix for a player's bug list, self-checked with pr_ready); harnesses run on main by parity-main, which gates releases.
They are official mods now (#1982). The Extras page loses their rows and
the HUD its built-in drawing. A player who had one on gets the mod on the
first start: installed from Horizon's registry, checked as the launcher
checks it (size, SHA-256, signature, manifest), turned on, with the
corner and options carried into the mod's storage. One chat line says so
("FPS counter moved to the mod browser: installed for you"). If it can't
be installed (offline), a line says Horizon tries again at the next
start. horizon-extras.txt keeps the old keys and lists what was done in
movedToMods.
The client's horizon-mods dependency gains the `index` feature for the
check. The frame and click counters now run only for mods.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On all 24, a cargo build held ludodesktop's CPU at 92 °C and the watcher kept pausing; at half,
harnesses run at a median of 80 °C.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`entity_geometry` ran `cracks()` twice per frame: once for the block-entity
pass that draws `render_breaking`, once for the overlay near the end of the
same function. Each run walked `mining` and the `destroying` map, and formatted
`block/destroy_stage_<n>` for every crack it found before looking that string up
in the atlas.
The list is computed once, at the top of the function, and the passes read it.
The ten stage names are a const table, so a crack costs one lookup and no
allocation. Nothing between the two passes touches `mining` or `destroying`,
which are only read there, so the list the second pass used to rebuild was
always identical to the first one.
No behaviour change: the same ten sprite names, and a stage above 9 still finds
no sprite and skips the crack.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
affected_here called parity.affected_harnesses inside the filter, once for
each of the 343 harnesses. Since the selection reads the Rust items each
probe reaches (#1964) one call takes about a minute and a half, so
pr_ready sat for hours before its first step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Those fields only appended, so the cursor, selection and arrow keys did nothing. A sign paste strips formatting first, matching TextFieldHelper.getClipboardContents.
nix-build names a dev output's GC root <name>-dev, so the runner's PKG_CONFIG_PATH pointed at links
that didn't exist and every client build failed on libudev; it now uses the store path nix-build
prints. The package temperature spikes for a second under load, so the heat check takes the median
of 5 readings. Restarting the watcher no longer throttles a running job.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In the idle class the thread still slowed a fresh world's first 600 chunks
by about 3 s on a 4-thread laptop (it shares a physical core with a worker).
It now waits until no proto chunk was generated for 2 s, or a minute after
the world opened, and gives up if the world closes first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wait_loaded declared a scene loaded during the idle second right after a teleport, so village and mountain were measured with 0 chunks. Port the guard from tools/perf_bench.py: counts must change once (or stay full for 5 s) and resident chunks must reach 80% of the view circle.
Co-Authored-By: Claude <[email protected]>
The menu op already lists the slots the click changed; a container slot
among them is the Slot.setChanged that saves the block entity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The item's fluid world had its own copy of the FluidTags.WATER/LAVA
membership the player's immersion uses; motion now exports that one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.distanceTo casts each offset to float and sums the squares in
floats; taking the double distance and rounding it gave a slightly
different damage at the edge of rounding.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a 4-thread laptop the background search at nice +10 took a share of the
cores from the chunk workers: 600 chunks arrived 3-4 s later. The thread now
runs only on cores nothing else wants (Linux SCHED_IDLE, macOS background
QoS, Windows idle priority): horizon_threads::idle_current.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Atlas patches rasterized while building world text (display text, name
tags, sign glyphs, map items) drained into the HUD batch before
entity_geometry ran, uploading a frame late: for that frame the quads
sampled unwritten atlas cells, so text flickered or showed the wrong
glyphs whenever content changed or new text entered view.
Display text also lined up 10 px apart where vanilla's lineHeight is 9,
its background quad stopped a pixel short of vanilla's -1..width+1 fill,
and name tags used 0x7F where renderNameTag's colours are 0x20FFFFFF
(discrete) and 0x80FFFFFF (see-through). lightCoordsWithEmission floors
only the block channel at 2 and leaves the sky alone, and a discrete
name's background draws lit in the normal pass rather than unlit.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The clean checkout removes target/, and vanilla-data.sh builds horizon-prepare only when it
prepares afresh, so compile-exporters had no binary on a cached preparation. The bundle is
also compiled in the job instead of reused from .local/exporters, which vanilla-data.sh links
to preparations that pull requests' parity jobs write, and the job fails if anything but our
classes and the manifest would leave the runner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The previous commit decides chunks far from the rings without them. The
128 biome searches now start on a helper thread as an overworld with
structures opens, behind the server and render threads, so an eye of ender
or a chunk near a ring finds them done or shares what is left, instead of
running them all on the thread that asked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Move the instant_digging regression from redundant PR #1966 into the existing stale-dig fix for #1880 and #1908.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Ludovic's rule (2026-10-26): runner_pause.py pauses at once on keyboard, mouse or gamepad input
(read from /dev/input, only whether bytes came), on an unlocked session touched in the last
15 min, at 90 °C, on a game or the GPU. Fixes the service start: the link loop's last test
failed the script under set -e.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ATLauncher saves Mojang's version JSON as is in configs/json/minecraft and
the client jar as the library net.minecraft:client:<version>, with the
libraries at their version JSON paths and the assets and Java runtimes in
Mojang's layouts. horizon-prepare now reads that layout, in the chosen
folder, the launcher above a chosen instances/<name> folder, or, with no
folder chosen, ATLauncher's default folder on each system after the
official launcher's, then the CurseForge app's Install folder. The lock's
pins gate it as before, and nothing is downloaded. The error names every
folder looked in and the launchers Horizon reads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The camera's death roll and zoom (GameRenderer.bobHurt, Camera.modifyFovBasedOnDeathOrFluid) read deathTime + partialTick. Horizon derived deathTime from the wall clock since the death message, which the server thread path never set: there deathTime stayed 0, so every frame used 0 + partialTick and the view rolled from 0 to 0.2 degrees and snapped back twenty times a second (with a matching zoom pulse) instead of settling at vanilla's 3.6 degrees. In frame, the wall-clock ticks were not in step with the frame's partial tick either.
deathTime is now the tick counter LivingEntity.tickDeath keeps: the in-frame tick already counted it, and the server thread path now resets it when the death screen opens and counts it once per client tick. horizon-ctl state reports it as player.death_time.
A server player's clicks in a furnace, brewing stand or crafter menu wrote
the block entity's slots but never marked it changed, so the chunk was
not saved: an idle furnace (nothing burning, so its own tick changes
nothing) given or emptied through its menu rolled back on reopen or
chunk unload, losing or duplicating the items. Comparators kept the old
reading too.
When the click changed the container's slots, call container_changed
(BlockEntity.setChanged, reached through Slot.setChanged in vanilla's
AbstractContainerMenu.doClick and quickMoveStack), as chest-like menus
already do.
A server's player on a trip into another level was skipped by
tick_guests before its zero-health body was marked dead, so a death
during the arrival (a /kill, damage) only counted once the destination
had loaded. The client already showed the death screen, and a Respawn
press in between was refused (guest_respawn wants a dead player) and
then lost, leaving the player on the death screen until a second press.
LivingEntity.hurt calls die at once, loaded or not: the death half of
the guest tick is now its own guest_die, which also runs for a player
on a trip.
Servers on 26.3 spawn NPCs as minecraft:mannequin — the avatar entity
vanilla added for exactly this — instead of fake players (gommehd.net's
"Gommunity" lobby NPCs are invisible today). The client never registered
the type: MobTypes.load only builds species::ALL, so AddEntity produced a
tracked entity but nothing to draw.
- EntityDataSerializers.RESOLVABLE_PROFILE (41) decodes into
Value::Profile. Before, the entry list ended at it and the mannequin's
DATA_IMMOVABLE and DATA_DESCRIPTION after it never decoded.
- species::ALL gains mannequin (player#main model, Humanoid anim), and
the player-only render paths — skin texture, sleep pose,
AvatarRenderer's 0.9375 scale — take an avatar() check instead of the
id string.
- SetEntityData's apply stores DATA_PROFILE on the mob, and
update_remote_skins resolves its textures property through
PlayerSkinRenderCache's rules (unsigned allowed, SkinManager's
insecure path) the way a tab player's skin resolves.
- Mannequins get the shared-flags crouch and sleeping-position handling
other players already had.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A player sprint-swimming up under a ceiling was sent back every tick on the server thread (and for LAN guests): the server never updated the player's pose, and handlePlayerPositionChange's collision check always used a standing 1.8-block box, so the 0.6-block swimming box the client moved with looked like it entered the ceiling. Player.tick ends with updatePlayerPose, and the check uses getBoundingBox, so run updatePlayerPose in the server player's tick and check moves with the pose's dimensions. Fixes#1899.
A used End gateway never worked again on the server thread or on a
dedicated server. TheEndGatewayBlockEntity's server ticker (portalTick)
ran only inside the client block-entity pass, which returns at once when
there is no renderer, so the 40 tick cooldown that
EndGatewayBlock.entityInside starts (triggerCooldown) never ran out.
A Game without a renderer now runs portalTick on the gateways it keeps a
server copy of, in block-ticking chunks as Level.tickBlockEntities does,
and sends triggerCooldown's block event every 2400 ticks of age.
A server's player sent into another level waits on a trip until the
chunks around its target load, and the server leaves its body alone and
skips its portal checks until then. A /tp within the level while the
arrival was still loading left that trip behind: its target lost its
loading tickets and never finished, so Nether, End and gateway portals
stopped taking the player until they went back to the abandoned spot.
ServerPlayer.teleport within a level places the player at once, so the
pending arrival is now abandoned. server-dimension-players gains the
case: sent far into the Nether, teleported back near the origin before
the far chunks load, then an End portal there.
The chat keeps where each message came from and the Chat option's
abilities. A message the option hides is still dropped on arrival, but a
change of the option now also hides the lines already in the chat that
it no longer shows, and brings them back when it shows them again
(ChatComponent.setVisibleMessageFilter -> refreshTrimmedMessages), on
apply_settings. Every Chat::add names its source. The Restrictions
screen reads its two flags from ChatAbilities instead of its own copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A chest minecart, hopper minecart or chest boat's menu edits a stand-in
container, which is meant to be copied back to the vehicle when the menu
closes. close_container took the open container before calling
close_cart_container, which then found nothing open and copied nothing:
items taken out came back on the next open, and items put in vanished.
Copy the stand-in back from the key close_container took. A server
player's clicks in such a menu copy it back at once, since the menu is
the vehicle's own inventory (ContainerEntity, AbstractMinecartContainer
and ChestBoat implement the container the menu reads and writes).
FireworkRocketEntity.dealExplosionDamage hurts every LivingEntity within 5
blocks that the rocket can see (its feet or its middle, ClipContext
COLLIDER with no fluids), besides the entity it is attached to. Horizon only
hurt the attached player and the nearby mobs, so a rocket bursting next to a
player on foot did nothing to them, in both single player modes.
The player (unless it is the attached one) and every guest in the level now
take damageAmount * sqrt((5 - distanceTo) / 5) as fireworks damage, with the
armor wear of hurtServer. distanceTo is a float, as vanilla computes it, for
the mobs too.
Vanilla has no /pregen, so with it always registered /help and the command
tree differed from vanilla's (the command_exec harness failed on /help).
Horizon's own commands are now added to the game's tree only when the owner
asks: `horizon-pregen=true` in server.properties (`horizon-pregen = true`
under [properties] in server.toml) for a dedicated server, `--pregen` for
the game (single player, LAN). Off, the trees are vanilla's own, unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Compile and validate original exporter bundles during release builds, cache development compilation, and reuse initialized JVMs and collision data during preparation. Keep legacy preparers compatible while current releases need only a Java runtime.
Co-Authored-By: Codex <[email protected]>
A mob that sees a guest (a LAN guest, or the player of the server thread)
holds Who::Player as its target, and what it does to that player runs with
the guest swapped in. A shulker's bullet, though, keeps the target it was
fired with until it lands, and the projectiles tick outside that swap:
there Who::Player is the host's own player, so on a server thread the
bullet found no target, stopped homing and fell onto the blocks.
The bullet now carries the guest's entity id, as ShulkerBullet.finalTarget
holds the targeted entity in vanilla (ShulkerAttackGoal.tick passes
Shulker.getTarget()), so it homes on the guest among the projectiles'
targets and its hit runs as that guest: 4 damage and Levitation.
The FPS counter and keystrokes extras rewritten as mods on the public API
(render, input, ui, storage), in mods/official/, with the extras' options
on their settings pages and the extras' look: the same boxes, colours and
text positions, pixel for pixel. They are workspace members, so clippy and
pr_ready cover them; a maintainer publishes them in horizon-mods. The
built-in extras stay until the removal PR, which installs these for the
players who had them on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clicking with a torch or flower into a space that is already taken (or where the block cannot stand) made the server-thread client play the block's place sound when the server resent the refused target. BlockItem.place returns FAIL before playSound when canPlace() is false, and the client only hears its own predicted placement, so clear the pending place sound when no placement was predicted.
- Skull keeps what its profile draws (the skin URL, its skins/<sha1>
sprite and the default skin), computed when the block entity loads,
instead of decoding base64 JSON and hashing it for every head every
frame; skull_skin_at no longer scans the chunk's saved block entities.
- BlockEntity::for_block gave minecraft:piston_head a skull block entity
(it ends in _head), which would have been saved as minecraft:skull.
- AGENTS.md is left as it is on main: the workflow note is not part of
this fix.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Share the asset index and verification state, stage checked imports while hashing, preserve indexed replacements, and clone independent resource copies where supported.
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
chicken_flap reads only the ground flag, so the test builds its mob from
mobs::sensor_scan_tests::kind() instead of a 50-line copy of a MobType.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
full_name reads the process's language, which the server thread shares
with its window; only a process with no language loaded (a dedicated
server, the probes) keeps the English names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
stats_scroll and stats_drag take the GUI height from gui_size() instead
of a copy of it. The Game Rules list's press, drag and release get the
test from #1913 (vlesmentie's take on the same fix).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Show the framed item's custom name through the existing world name-label renderer, matching vanilla's HUD and crosshair conditions. Extend the LAN frame check for normal and glow frames.
Co-Authored-By: Codex <[email protected]>
The generated banner reuses save.rs's BannerBlockEntity.loadAdditional
(now decode_banner) instead of a copy of it, and the generated block
entity clones its NBT only for the kinds that read it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
runner_pause.py pauses on any seat0 session (locked too), games, GPU use or a pause file, and is
the only thing that starts the runner; a job under way is throttled, then the runner stops. The
desktop host in parity_hosts.json refuses work while paused and its workers stop on a pause. The
nightly audit stays on the Mac mini. Docs and a unit test of the watcher.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
runner-env.sh and vanilla-data.sh find Python 3.13, the pinned JDK and sccache on NixOS;
linux-runner.sh installs a runner as a systemd user service with its own screen; runner-pause.py
stops it from taking jobs while a game runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The built-in extras become official mods; these are the calls the FPS
counter and keystrokes overlay need to look and behave the same:
- render.set-corner(corner, w, h, shapes): a box the game places with
its own corner stacking (under the status effects, with the overlays
and hud panels), clear-corner; text-width(text) in the HUD font
(advances for U+0000..U+024F, 6 otherwise); frame-stats(): the game's
FPS counter (fps, min, 1% low).
- input.mapping(name): a movement or click mapping's key label and
whether it is held; clicks-per-second() for attack and use.
- ui: a button widget (200 x 20, select(id, 0)) for settings pages.
- The game keeps its FPS and click counters running while a mod runs,
and refreshes the snapshot every frame for mods with a frame hook.
- The ui test guest gains /corner and /readings, with a sandbox test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Follow Leashable.tickLeash for dying mobs and cover trader/player-held llama leads through the death animation.
Co-Authored-By: Codex <[email protected]>
A version's resources were reused only when the preparer's sources and the exporters were
byte for byte the same, so any change to crates/horizon-prepare (even one that prepares the
same files) made every player prepare again. The launcher now compares the number the preparer
declares for what it makes (`pub const OUTPUT_VERSION: u32` in crates/horizon-prepare/src/lib.rs,
bumped by hand when the output changes and checked by CI in the follow-up). Preparers from
before the number are known by a fingerprint of their sources and exporters: v0.2.49 to
v0.2.64 prepare version 1's output (measured on the CI runners' preparations). Older versions
are compared as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Deliver anvil level events to the acting player and keep their local sound out of positional sound capture. Extend the anvil LAN check to count sounds for both players using either player’s anvil result.
Co-Authored-By: OpenAI Codex <[email protected]>
The particle engine copied the GUI atlas's sprite rects once at start; a pack or server-pack
change rebuilt the atlas but not the engine, so every named and set sprite then drew a stale
region — break crumbs showed other blocks and campfire smoke showed random items (#1923).
The engine now re-reads the rects and the packs' sprite sets on reload, and a particle whose
sprite-set index outlived the rebuild keeps its sprite instead of indexing past the new list.
The reload also layers content mods over the root again, which startup does but reload forgot,
so the first reload silently dropped their assets and data.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Keeps both sides of #1823 (wither rose) and this branch (lava and water
cauldrons) in motion.rs's inside-block match, the MotionEvent variants,
game.rs and lan_actions.rs, and all three tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both lists drew a scroll bar but only listened to the mouse wheel: a press
inside the bar column, the drag that follows and the release were dropped.
`ui::Scroll` already models AbstractSelectionList's press/drag/release (the
server list uses it); the two screens now keep their scrolling in it and the
window passes the press, the pointer's moves while it is held and the release
along.
Fixes#1782
- WIT `input`: claim-items(ids) makes the use button on those items the
mod's: use-start, then use-stop with the ticks held (button up, item
out of the hand, or a screen opening); the game does nothing else
with the click. register-key(name, label, key) for up to 16 keys
(key.keyboard.r), heard as key-down / key-up while playing; the
game's own mapping on the key still runs. held(), poll() (256 kept).
- game/mod_input.rs: play_mouse asks mod_use first (local and remote
windows), the tick ends a use whose item left the hand, the keyboard
and horizon-ctl's key send the physical key to mod_key.
- No claim and no key, no cost: one check each.
- Experimental: the manifest needs experimental = ["input"].
- The ui test guest gains /input_claim, /input_poll and /input_held,
with a sandbox test. Docs: mods.md, stability.md, the changelog, the
regenerated reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep framebuffer limits in rendering and explicit option cycling rather than overwriting the saved choice during startup or settings application.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The snapshot's key hashes the 70 MB of source tables it was resolved from.
The client's tables and the world generator's are loaded from the same
folder, so a world opening hashed them a second time: a third of a second
of the server thread's start on a 4-thread laptop, before any chunk. The
hash is now kept for the process, by each file's length and modification
time; a changed file is read again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 128 stronghold biome searches (two seconds of CPU on a 4-thread laptop)
ran when the first chunk of a new world looked up its structure starts, so
nothing near spawn was generated until they were done. Each search ends
within 8 chunks of its ring chunk, which is known before searching, and
strongholds lie 1,400 blocks out or more: a chunk with no ring search in
reach (exclusion zones included) is now decided without them, which is what
they would say. They run when a chunk near a ring, /locate or an eye of
ender needs them (ring_positions now searches on demand instead of returning
nothing before a lookup did).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Placed player_head blocks kept no profile: skulls had no block entity, so the
renderer fell back to a hardcoded steve for every head. Give skulls a real
BlockEntity carrying vanilla's SkullBlockEntity fields (profile,
note_block_sound, custom_name) so the placement pipeline lands
minecraft:profile in it the way block_entity_item already maps, encode and
decode it through save so regions keep it, send its update tag to remote
clients, and resolve the profile through the skin downloader the head items
and the tab list share: the downloaded skin slot once it lands, the
profile's DefaultPlayerSkin uuid pick while it fetches, SKIN_BY_TYPE steve
without a profile.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
When an agent is asked for a feature it runs the whole loop itself: check open
PRs for the area first, work in a new worktree on the latest upstream main,
build and run pr_ready, then push to the fork and open the pull request
against upstream, never the fork.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Nothing tracked `ItemCooldowns`: `draw_item_decorations` passed a cooldown of
0.0 with a note that there was none to shade, and ender pearls were held back
by an ad-hoc `pearl_cooldown` counter.
`horizon_client::cooldowns` keeps the `use_cooldown` component's entries by
cooldown group the way `Player.getCooldowns` does (start and end ticks, the
percent `getCooldownPercent` returns), `ItemInfo` now carries the item's
`use_cooldown`, and the game uses it where vanilla does:
`ServerPlayerGameMode.useItem`/useItemOn refuse a stack on cooldown,
`ItemStack.applyAfterUseComponentSideEffects` starts the entry when a use
finishes (a thrown pearl, a wind charge, an eaten chorus fruit),
`GuiGraphicsExtractor.itemCooldown` shades the slot, and a server's guests
keep their own and are sent `ClientboundCooldownPacket` (started, and 0 when
it ends), which a client applies.
Fixes#1763
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
The registry resolved every `description` from the pinned `en_us` map at load
and `full_name` took `enchantment.level.<n>` from the same table, so a tooltip
showed an English enchantment line whatever language the player picked.
Keep the description's translation key and resolve it, and the level, through
the current language at display time, with the English text as the fallback for
the server thread, the probes and an old resource root.
Refs #1774.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The Chat Settings option was saved and shown but never applied: Hidden and
Commands Only still sent and displayed ordinary chat, and Hidden still
displayed the server's messages.
Minecraft.computeChatAbilities turns the option into ChatRestriction's
permissions, and they are now honoured where vanilla honours them:
ChatScreen refuses to submit input they take (hasAllowedInput) and says why
in a red usage line, ClientPacketListener.sendChat/sendCommand stop it before
the packet, ChatListener drops the player, server and client sources the
option hides, and ClientInformation carries the visibility to a server (at
login, and again when it changes, as Options.broadcastOptions does).
Fixes#1376
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
Clear completed guest digging so replanted flowers and crops are not broken again after attack is released. Add a LAN replanting regression for both single-player paths.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Mods draw through the game's own renderer, never their own shaders:
- WIT `render`: set-hud(shapes) with fill, quad (any four corners),
sprite (the game's GUI sprites), item (scaled) and text shapes, up
to 1024, kept until replaced; gui-size(); shake(strength, seconds).
- Drawn over the HUD after the extras, under chat and screens, hidden
with F1 (game/mod_render.rs). Colours are 0xAARRGGBB.
- shake turns the view, not the player: jolts fade out squared, add up
to 1, at most 3 degrees of yaw and 2 of pitch, scaled by Distortion
Effects (screenEffectScale; 0 turns it off).
- Experimental: the manifest needs experimental = ["render"].
- No mod drawing, no cost: an empty iterator and an is_empty check.
- cargo horizon-mod check reports shakes and HUD shapes. The ui test
guest gains /render_hud, /render_clear and /render_shake, with a
sandbox test; HudBatch gets fill_quad.
- Docs: mods.md, stability.md, the changelog, the regenerated reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On top of the lava cauldron's inside test: water and powder snow
cauldrons are no longer skipped. Their entity-inside shape is the
cauldron's or their content, Block.column(12, 4, 6 + 3 per level), and
LayeredCauldronBlock.entityInside applies: on a server, a burning
player first asks for handleEntityOnFireInside (runBefore EXTINGUISH),
which lowers the fill level (lowerFillLevel, a powder snow cauldron
turning to water of its level first, block_change with no source); then
EXTINGUISH (Entity.clearFire) puts the fire out.
Fixes#1330.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entities Horizon's entity culling hides from the camera keep their vertices in a hidden span that
only the pack's shadow pass draws; the local player's model is built in first person, hidden from
the camera, while the pack casts entity shadows. shadowPlayer draws the player alone when
shadowEntities is off, shadowLightBlockEntities the light-emitting block entities when
shadowBlockEntities is off. Without a shader pack nothing changes.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, §2.4, §3.2; no other
loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the rows along a box's bottom, top and sides every cell is outer, so in a
section no block reaches out of, the rest of the row up to the chunk's edge
is passed in one step instead of cell by cell. The candidate budget still
ends the scan where it did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review on #1864: when a mod stops while its screen or settings page is
up, draw_mod_screen dropped the player back to Playing without
capture(true), leaving the mouse free until the next click into the
world. The dead surface now leaves through close_mod_screen, which puts
back the screen it covered and holds the mouse on a return to Playing;
the same goes for a screen dropped while it was up.
mod_ui_view also cloned the title and every widget (up to 64, lists to
512 rows) on each frame, click and scroll. It now borrows the mod's
stored widgets — the screen's scrolls live in a disjoint field — and the
click finds the picked row first so the &mut ui_select runs after.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The leaf's tilt ticks, its reset and its redstone reset were already in
horizon-world (big_dripleaf_tick, big_dripleaf_reset_tilt), but nothing
started them: BigDripleafBlock.entityInside was missing. An entity on
the ground whose feet are above the leaf's y + 0.6875 (canEntityTilt)
tilts a flat, unpowered leaf UNSTABLE and schedules its tick in 10.
The block tick's inside scan now also passes the host's and the LAN
guests' feet and on-ground flag to Behaviours::entity_on_big_dripleaf.
Fixes#1683.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sculk sensor, shrieker or catalyst with no vibration chosen, travelling or
waiting, and no charge cursors, ticks without changing anything (vanilla's
VibrationSystem.Ticker.tick and SculkSpreader.updateCursors return at once).
The tick now leaves those out before asking whether the chunks around them
tick (nine lookups each) and before looking them up a second time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A structure template saves a banner's layers in its block entity NBT, and
StructureTemplate.placeInWorld hands that NBT to the level, where
BannerBlockEntity.loadAdditional reads `patterns`. Horizon built the client
block entity from the block alone (BlockEntity::for_block), so the End
cities' magenta banners came out bare.
Fixes#1438.
Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
BlockCollisions keeps a block on the cells around the box (Cursor3D types 1
and 2) only if it has a large collision shape or is a moving piston. Most
sections hold neither in their palette, so the scan now asks the provider
once per section and passes those cells without reading them: a mob's box
reads about a fifth of the cells it did. Same hits, same order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The loot rolls carry enchantments and potions (horizon_world::loot runs
enchant_with_levels, enchant_randomly and set_potion), but chests filled
when a structure is placed, and mineshaft chest minecarts, turned each
LootStack into an item with loot_item, which keeps only the item, count,
damage and exploration map. End city gear came out bare. Both now use
loot_item_with_components, as vaults, trial spawners and decorated pots
already do.
Fixes#1437.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A stack picked from a creative tab exists only on the client. Thrown out of
the creative screen as a server's client, it went out as a ContainerClick to
slot -999, which the server had no carried stack for, so nothing dropped. The
client now sends handleCreativeModeItemDrop's SetCreativeModeSlot(-1, stack),
and the server drops it from the player's hand, with vanilla's drop spam
throttle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
26.3's AbstractWidget.handleCursor asks for CursorTypes.POINTING_HAND
when a widget is hovered, or NOT_ALLOWED when it is inactive, and the
frame applies the last request to the window (GuiGraphicsExtractor
requestCursor and applyCursor, Window.selectCursor). Horizon never
changed the cursor, so it stayed an arrow over buttons.
widgets.rs keeps the frame's request, ui::button and the resource pack
screen's buttons make it from the mouse hover (not keyboard focus), and
the game sets winit's Pointer, NotAllowed or Default cursor after each
frame when it changes.
Fixes#1347.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server mod's registration now lists each command's arguments: a name, a
kind (word, text, integer with bounds, number, boolean, player, block-pos)
and whether it is optional. The server adds them to the command tree every
player receives, after vanilla's commands, so a player's chat box completes
and colours them as vanilla's. When a player sends one, the server parses
the line as vanilla's parsers do (horizon-mods' commands.rs: ~ coordinates
from the player, @s/@p, integer bounds) and calls the mod's command hook
with the values by name, or answers with vanilla's error without calling
the mod.
The hub keeps the server mods' commands as a root in the commands report's
format and resends players' trees when they change (CommandTree::
packet_with); the game sets it when its mods start, stop or reload, or a
hub opens. In-frame, the window's own chat box lists them by name.
SDK: Registration::command_with(name, [Argument::...]), Argument builders,
Args (text(), integer(), player(), block_pos() ...); command(name) is free
text, an optional text argument. Examples: welcome's /mined [player],
beacons' /beacon [height] and /ground [pos], with scripts for values and
errors.
Docs: docs/mods.md (typed commands, kinds table), docs/api/mods.md and
the regenerated reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
shadow_entities and shadow_block (each falling back to shadow) load with
the entities' vertex inputs unless shadowEntities or shadowBlockEntities is
false (spec 2.4, 3.2; both on by default, OQ-19). They draw the same entity
and block-entity runs the gbuffers programs would, with the same per-draw
uniforms, into the shadow map after the solid terrain and before the
shadowtex1 copy. A run is uploaded when a gbuffers program or a shadow
program draws it; Horizon still draws the runs no gbuffers program covers.
Without a pack nothing changes: only the pack's entity code is touched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chicken.aiStep updates flap and flapSpeed on the client too, for the
chickens it only displays. tick_remote_entities never did, so with
single player on its server thread (and on servers) a falling chicken
kept its wings folded. The wing part of chicken_ai_step is now
chicken_flap, and the remote tick runs it from the server's on-ground
flag; the falling slowdown and egg laying stay the server's.
Fixes#1706.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Server mods get interface world: get-block(dimension, x, y, z) reads a
loaded chunk's block state as "minecraft:oak_log[axis=y]", get-biome its
biome ("minecraft:plains"), and
set-blocks(dimension, changes) sets up to 4096 blocks per call, checked at
once, made as /setblock makes them (place_state: neighbours, light, block
entities, every client told through the block log) after the call, before
that dimension's next tick.
Reads cost no copy: for a mod call the game lends Mods the levels' chunk
maps (moved out with mem::take and put back after), only while a running
mod has world. Writes for a level that isn't ticking (another dimension,
in-frame) wait for it, up to 65,536.
Test scripts get blocks (setup) and expect.blocks; cargo horizon-mod check
reports a trial run's block writes. Example beacons: /beacon builds a
glowstone pillar beside the player, /ground reads the block underfoot and the biome.
parse_state is now a free function so the mod path shares it.
Docs: docs/mods.md (the capability, script keys), docs/api/mods.md and the
regenerated reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game that simulates a world now starts its server mods (API 0.3,
experimental): single player's server thread, single player in-frame
(beside client mods), a LAN host either way, and the dedicated server.
Windows that are clients of a server never run them.
game/server_mods.rs: tick_server_mods at the end of the tick diffs the
players (player_joined, player_left), delivers the tick's chat and block
events in order, then calls tick. Block events come from players' breaking
and placing (the acting player is the guest during as_guest); chat from the
local chat path and the hub's new HostEvent::PlayerChat; a guest's /command
reaches its mod before vanilla's dispatcher. tell and command replies go to
their player (the window's chat or a SystemChat); broadcast goes to every
player and the log.
--dev-mod now works on the server thread, and servers check a dev mod's
file once a second from their tick. Every hook returns at once when no
server mod runs; without the mods feature they are empty stubs.
Docs: docs/mods.md (where server mods run, when hooks run, not yet),
docs/api/mods.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CopperGolemStatueBlock.useItemOn: anything but an axe, an empty hand too,
cycles copper_golem_pose with its sound and block_change game event; honeycomb
on a weathering statue passes on to waxing. Axe scraping, unwaxing and
stripping and honeycomb waxing now count the item as used, and waxing sends
its block_change game event, as vanilla's useOn does. The use_item_on harness
gains statue cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shadow program now draws the world's terrain from the light's view.
The shadow pass culls its own section list against the shadow camera's
frustum (render distance, capped by shadowDistanceRenderMul), nearest
the light first, and draws only the faces toward the light. Its draws go
by arena page within 64-block bands from the light, as the camera's solid
draws do, so the flat path draws them in about ten calls per list instead
of one per page change (over a thousand with BSL). Its draws
follow the camera's translucent draws in the same instance and indirect
buffers, so the flat path gives them calls of their own. Sections never
sorted for the camera are sorted once so their translucent indices are
valid.
The frame's first shadow pass clears the shadow map and shadowcolor
buffers as it begins, instead of a separate pass that loads and stores
them all; the translucent shadow pass is timed under its own label.
Without a pack nothing changes: the shadow lists are empty and the
terrain stays one draw call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pregen::new counted the area chunk by chunk and split the strips with
an i32 product: /pregen start 30000000 overflowed, then hung the server
thread for hours. Rows are now counted from the circle's half-width,
the strip edges in i64, and each strip starts and ends at the rows the
circle reaches, so a band never walks empty corner rows. Unit tests
check that every chunk of squares and circles is swept once and that
the largest radius plans at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Launchers like HMCL rewrite versions/<v>/<v>.json in the official layout
(same jars and libraries, their own fields), and horizon-prepare refused it
as a version Mojang may have republished. Mojang's copy saved as the imported
manifest.json is now used instead, and without one the error says the
launcher may have rewritten it and how to get Mojang's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The `shader` kind's check (index::inspect) reads the .hshader and runs
horizon_shader_api::check, the same check the game runs before it loads a
pack, so the registry's CI (`cargo horizon-mod check` / `registry build`)
refuses a pack the game would refuse, with each problem at its file and line.
Its facts are the manifest's id, version and shader API line. The kind's
files are .hshader, `cargo horizon-mod check` recognises them, and
registry::open takes shader submissions. Installing one from the launcher
stays refused (check_file) until the launcher installs shader packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the shadow pass runs, the pack gets real shadow buffers (spec 6.3):
shadowtex0 (cleared to 1.0 every frame), shadowtex1 (a copy, made only when
a program reads it) and shadowcolor0/1 (each with two sides, made only when
used, cleared to their ShadowClear colour, white by default), square at
shadowMapResolution within the device's limit.
Samplers: shadowtex0/watershadow, shadowtex1, `shadow` (shadowtex1 beside
watershadow) and the HW forms read the shadow map, through a linear depth
comparison when declared sampler2DShadow; shadowcolor/shadowcolor0/1 read
the colour buffers. In the shadow programs, buffer n is shadowcolor n and
colortex reads a stand-in. Without the shadow pass nothing changes: the
names read the stand-ins as before.
shadowcomp..shadowcomp99 run after the shadow pass and before prepare, each
drawing into the alt side of the shadowcolor buffers it writes and flipping
them after, as composite programs do with colortex (6.1.6). The shadow pass
itself draws nothing yet, so the map stays clear.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 2.1, 5.2 (samplers), 6.1.6 and 6.3; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shadow pass runs when some program reads a shadow buffer (shadow,
watershadow, shadowtex0/1, their HW forms, shadowcolor, shadowcolor0/1) or
shadow.enabled=true; shadow.enabled=false keeps it off (spec 2.5). Then
`shadow` (solid and cutout terrain), `shadow_water` falling back to
`shadow` (translucent terrain) and shadowcomp..shadowcomp99 load, in the
new Shadow and ShadowComp groups (2.1, 2.4). shadow_solid and shadow_cutout
are not used: the game's solid and cutout terrain is one draw.
Their output n writes shadowcolor n (6.3): draw buffer lists stop at the
first index past 1. translate_shadow gives a shadow program the shadow
camera's matrices as its model-view and projection (5.2). The game does not
draw any of them yet.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 2.1, 2.4, 2.5, 5.2 and 6.3; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The stage files' shadow constants (shadowMapResolution, shadowDistance,
shadowDistanceRenderMul, shadowIntervalSize, sunPathRotation, shadowMapFov,
shadowNearPlane, shadowFarPlane, shadowHardwareFiltering[0|1] and the
shadowcolor0/1 Format, Clear and ClearColor) are read into Config::shadow
(spec 6.3, 7.1).
The shadow matrices are no longer a placeholder (OQ-6): the camera looks
from the shadow light, sits 128 blocks toward it from a point that moves in
steps of shadowIntervalSize along the shadow view's own axes, and projects
orthographically over shadowDistance each side (or in perspective with
shadowMapFov), between shadowNearPlane and shadowFarPlane.
sunPathRotation turns the sun and moon's path about the world's X axis:
sunPosition, moonPosition, shadowLightPosition and the shadow camera follow
it, and so do the sun, moon and stars a pack's gbuffers_skytextured and
gbuffers_skybasic draw.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 6.3 and 7.1; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A test script drives a server mod as a server would: join = "Alex" brings a
player in (ids stable per name, so a rejoin is the same player), as = "Steve"
picks who acts (the first player on otherwise), and player, chat, command,
break and place are that player's doing; leave = "Alex" lets them go. Server
expectations: told (every players.tell, "Name: text") and broadcast; feedback
and error read the reply to the player who typed the command. Ticks call both
kinds of tick hooks, so one script format serves client and server mods.
The welcome example gets two scripts (greeting, a newcomer announced once
even after a reload, /online across dimensions, /mined per player), run by
the sandbox tests with coords-hud's; docs/mods.md lists the server keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server mod runs where the world is simulated. Its manifest says side =
"server" and api = "0.3" (only a server mod may), and its component targets
the new WIT world horizon-server-mod:
- imports log and storage as client mods do, and the new capability players:
online() (id, name, position, dimension, game mode, health), tell(id, text),
broadcast(text), 8 lines per call shared with chat;
- exports server-events: init, tick, player-joined, player-left, chat, command
(with the player who typed it; its feedback goes back to that player),
block-broken and block-placed (with the block state as text).
horizon-mods: a second bindgen world sharing log and storage with the first;
a running mod holds either kind of hooks; Mods::load_for(Runs) starts only the
mods a game runs (the window client mods, the world's server server mods,
in-frame single player both) and marks the others Elsewhere. Client and server
commands are owned separately. The game passes Runs::Client for now, so
nothing changes in game until its server code calls these hooks.
The SDK gains horizon_mod_sdk::server (ServerMod, Registration,
export_server_mod!), the example server mod welcome (greets, announces
newcomers kept in storage, /online, /mined), and cargo horizon-mod check's
trial run plays a server mod with a player joining, chatting, typing its
commands and leaving. The manifest refuses client capabilities on a server
mod and players on a client mod. The generated reference, the guide, the
portal and the stability table describe it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.globalLevelEvent (an end portal opening, 1038; the dragon's
death roar, 1028) went to the Game's own audio without a position, which
is never forwarded: on the server thread, LAN and dedicated servers
nobody heard them. They now go to every guest as a global LevelEvent,
which a client plays two blocks from its camera towards the event
(LevelEventHandler.globalLevelEvent).
ClientPacketListener.handleExplosion plays the packet's explosionSound;
the client always played entity.generic.explode (a vanilla server's wind
charge sounded like TNT).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerInput was rewriting the server sprint flag from the Ctrl and forward
keys. That flag is synced back onto the client, and jumpFromGround adds its
0.2 look-direction impulse only while the flag is set, so releasing Ctrl
before the jump (or double-tapping forward) produced a plain hop. Sprint
still starts and stops through PlayerCommand.
Co-Authored-By: Grok <[email protected]>
Co-authored-by: 一水合氨NH3-H2O <[email protected]>
A mod's tests/<name>.toml scripts run in the sandbox the game uses, with no game
and no window. The runner plays the game's part (the player and clock the mod
reads, ticks, frames, chat lines, commands, a reload) and each step checks what
the mod did: its commands, chat lines, command feedback and errors, HUD panel
and corner, or that it was turned off. A failing step names itself and prints
expected against got. Each script runs on a scratch copy, so storage starts
empty and is removed afterwards.
horizon_mods::testing (feature runtime) holds TestGame and Script; the cargo
subcommand builds the mod (or takes --hmod), filters scripts by name, and fails
when one does. The template gains tests/hello.toml, coords-hud gains panel and
where scripts, and the sandbox tests run the example's scripts and check the
runner's failures, reload and expected stops. Scripts don't care what language
the mod is written in, so script mods (Lua) get the same runner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TrappedChestBlock.getSignal is the chest's opener count (at most 15);
TrappedChestBlockEntity.signalOpenCount updates the blocks around it and
below it when the count changes. GameLevel answered 0 for every trapped
chest. BarrelBlockEntity's openers set open=true for the first one and
open=false when the last one leaves; nothing set it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NetherPortalBlock.entityInside runs on the client's own level too: its
setAsInsidePortal drives LocalPlayer.handlePortalTransitionEffect (the
purple overlay, block.portal.trigger). The remote motion loop dropped
MotionEvent::InsidePortal, so single player on the server thread, LAN
guests and dedicated servers' players went through portals without it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the band hasn't moved for 30 seconds the task logs how many chunks
of its first row are not loaded, being lit, unlit with no light job or
not post-processed, with the streaming stage counts, and queues light
again for unlit chunks whose neighbours are all loaded.
pause-when-empty-seconds paused a server with nobody online after a
minute, and a paused server skips its tick: post-processing stopped, so
the band waited forever on a row of lit chunks. A running /pregen now
counts as activity; the countdown starts once it is done.
A Chunky-like command for single player and servers: /pregen start
<radius> [square|circle] [<x> <z>], status, pause, continue and cancel,
with progress (chunks, chunks/s, time left) in the log and to the player
who started it.
The area is swept strip by strip by a band of rows that streaming treats
like a player's area (Streaming::set_band). A row is done once its chunks
are lit and post-processed; the rows behind the band then unload and are
saved, so memory holds only the rows in flight. The band's chunks are
ranked for decoration row by row as they come within reach, so a task
decorates in the same order however its jobs are timed. Between ticks
the server waits on the workers' results and schedules the next jobs at
once instead of once a tick.
Invalidate version 3 overlays that can omit untouched vanilla resources. Cover folder and ZIP pack cache recovery, and use a writable timestamp handle in the Windows cache test.
Co-Authored-By: OpenAI Codex <[email protected]>
A pack with gbuffers_hand (or textured_lit, textured, basic; spec 2.3)
draws the first-person hand and held items after the opaque world, into
its own buffers and the world's depth, without blending and with the
entities' 0.1 alpha cut. The vertices are Horizon's hand geometry, read
as entities are; `gl_ProjectionMatrix` is the hand's own projection (the
hand's field of view and bob) with depth scaled by MC_HAND_DEPTH, while
`gbufferProjection` stays the world's (5.2). Horizon's own hand draw is
skipped only when the pack draws it.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`item.properties` (spec 4.1) gives `heldItemId` and `heldItemId2` the
id of the main and off hand's item: -1 for an item the file lacks or an
empty hand, 0 without the file (5.3). `heldBlockLightValue(2)` are the
light levels of the held items' blocks, as Horizon's dynamic lights
already compute them. With `oldHandLight` (on unless the pack turns it
off, OQ-21) the first id and light are those of the brighter hand.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack with gbuffers_weather (or its fallbacks textured_lit, textured,
basic; spec 2.3) draws the game's rain and snow after translucent
terrain, blended, with vanilla's 0.1 alpha cut. The weather quads become
entity vertices with their UV animation applied on the CPU (the same
phase Horizon's weather shader computes; a test checks it against the
weather columns bit for bit), and rain and snow each read a copy of
their sprite, so the pack's `texture` repeats down the column as a
standalone texture would. The shared atlas gains COPY_SRC for that copy.
Without a pack nothing changes: Horizon's weather draw is skipped only
when the pack draws it.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack with gbuffers_clouds draws Horizon's cloud geometry itself (spec
2.3, OQ-3: no fallback), blended, after the opaque world. Horizon's own
cloud draw is skipped then, also when the pack's program writes nothing
(BSL and Complementary hide vanilla clouds that way). Without a pack the
cloud draw is unchanged.
The translator now rewrites a bare `discard;` or `return;` to
`if (true) discard;` / `if (true) return;`, so code a pack leaves after
it stays reachable for naga ("Expression not visited"). This also fixes
Complementary Unbound's gbuffers_skytextured.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From spec-yungsapi rev 3 §3, §3.1 and §3.2 (impl-questions IQ-2, IQ-3):
- the start piece does not count toward max_count;
- is_priority entries are tried first, in the pool's shuffled order, without
a weighted draw; min_required_depth skips a pick (no draw) while the parent
piece is shallower; ignore_bounds places without the free-space test (its
box is still taken, IQ-12);
- the fallback was already shuffled only when reached.
The port no longer counts these three fields as lost. The layout harness gets
yung-probe and yung-probe2: the spec team's probe pack (built from the oracle
profile) carried as a mod, compared start by start with the oracle's census:
384 of 384 and 32 of 32 starts match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack now draws particles with `gbuffers_particles` (falling
back to `gbuffers_textured_lit`, `gbuffers_textured`, `gbuffers_basic`)
and the entity parts that blend, a sneaking player's faded name tag and
entity shadows, with `gbuffers_entities_translucent` (falling back to
`gbuffers_entities`). Both blend with the translucent default unless
`blend.*` says otherwise; particles keep vanilla's alpha cut at 0.1.
Name tag text draws with `gbuffers_entities`, as cutout (2.3).
Particles draw after deferred, with translucent terrain, unless
`particles.ordering=before` or `particles.before.deferred=true`;
translucent entities draw before deferred unless
`separateEntityDraws=true` (3.2).
Without a shader pack nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack now draws entities with `gbuffers_entities` and block
entities with `gbuffers_block` (or their fallbacks, spec 2.3), after
terrain. Each run of the entity spans is one draw with its own
`entityId` or `blockEntityId` and `entityColor` (the hurt red, the
white flashes) in a per-draw slot. Horizon still draws everything else
(particles, cracks, beams, name tags, shadows, End portals, clouds) in
its own single entity draw, without the runs the pack drew.
The entity vertices are read again at set 1, so the pack gets the face
normal where a vertex has none (block entities), `at_tangent` along the
texture's u and `mc_midTexCoord`, as for terrain.
Entities and block entities draw without blending, with the loader's
alpha test at GREATER 0.1, as cutout (OQ-23); name tags and shadows,
which blend, stay Horizon's for now.
`HORIZON_SHADER_PACK_TIMING` no longer panics when a pass skips a whole
report window (no entities in view).
Without a shader pack nothing changes: the split only runs while a pack
draws entities.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Gbuffers programs get the loader's alpha test on output 0 when they
write `gl_FragData`/`gl_FragColor` (spec 3.2 `alphaTest.<program>`;
with `out` variables the pack tests alpha itself). `GbuffersInputs`
carries the geometry's default, none for terrain, water and the sky so
far; `alphaTest.<program>` replaces it for that program (`off`, or a GL
comparison and a reference).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The previous tests of litho.rs are deleted unread (clean-room ruling of
2026-10-25). The new ones build their own block registry from a facts
table and run each processor through process_list against a one-block
world: block_swap's property copy (CTOV's beetroots), references in
order and discard_input, the probe pack's nested condition with else,
apply_random per piece and per block, set_block's providers and its
block entity modifier.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mod world now takes each saved start whose pieces are all jigsaw pieces
as the oracle saved it (chunk_dump's HORIZON_SAVED_STARTS, through a new
GeneratorWorker::use_saved_start and PoolPiece::from_saved), so the blocks
are checked whatever the layout: Lithostitched lays jigsaws out its own way
(spec-lithostitched OPEN-L5) and the layout harness checks layouts apart.
Lithostitched's wrapper elements place their delegate; 1.21.1 block states
are read in 26.3's shape.
Counts are split between positions inside a piece's box and around them
(terrain adaptation, decoration), and every mismatch class is written to
.local/structure-layout/<profile>/blocks-report.json.
A CTOV profile carries Lithostitched and ChoiceTheorem's Overhauled Village
from a pack's jar folder (.local/structure-oracle/pack-jars).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clean-room implementation from spec-lithostitched.md §3-§5 and spec-ctov.md §4.
The game reads Lithostitched's processors as its data writes them
(processor/litho.rs): block_swap (only the properties the new block can
have: ripe wheat becomes ripe carrots, beetroot keeps its default age),
reference, apply_random, condition (with else, one processor or a list),
set_block (simple and weighted providers, preserve_state, block entity
modifier) and discard_input; the conditions all_of, any_of, not,
random_chance, true and matching_blocks (input or location, properties).
Their random follows random_mode per block or per piece; Lithostitched's own
seeding is open (OPEN-L6), so a positional random of the world seed and the
processor stands in (impl-questions IQ-7).
horizon-port-pack's new lithostitched module carries the processors as they
are, with the processors nested in them carried, drops those the game cannot
read (lists named by tag, other providers) as Java processors, turns CTOV's
modular_compat into a reference when its mod is ported along (else nothing),
maps lithostitched:jigsaw to vanilla's jigsaw (vanilla layouts, the decision
on OPEN-L5), unwraps limited/guaranteed/delegating elements to their
delegate (limits counted as lost until the next PR) and writes
add_template_pool_elements modifiers as horizon:add_to pools when their
NeoForge load conditions pass.
Deleted unread: legacy_structure_mods/lithostitched.rs and horizon-world's
structure/processor/lithostitched.rs. Blind edits to the legacy folder,
located by exact text or the compiler's error position: its module line
removed, dead_code allowed on ctov (its modular compat is unused now) and on a
test helper, and ctov's modular compat test ignored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While a shader pack is loaded, building the entity geometry records where
each entity's vertices begin, with its render stage (entities, block
entities, particles, breaking cracks) and its `entityId` or
`blockEntityId` from the pack's `entity.properties` and
`block.properties` (65535 for a name the file lacks, 0 without the
file). The spans reach the renderer in `Frame::entity_spans`, for the
pack's per-entity draws that follow; Horizon's own entity draw stays one
call and without a pack nothing is recorded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The clean-room structure oracle saved, for each start of a mod world, the
blocks of its box and every position around it that differs from the no-mod
world of the same seed. tools/test_structure_blocks_parity.py generates the
same chunks in full with Horizon twice (26.3 alone, and with the mods carried
by horizon-port-pack as players' packs are) and compares: a position counts
only where Horizon's no-mod world equals the oracle's (1.21.1 and 26.3 differ
elsewhere), and a mismatch is reported by its no-mod, expected and Horizon
blocks, so a missing processor rule reads as one line.
Better Dungeons (3 seeds, 96 complete starts): every structure is listed as
known, with the reason: the placeholder processors, the biome modifier and the
mod's own structure types are not implemented yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three things kept Sildurs Vibrant black or wrong:
- Matrix and array varyings (`varying mat3 tbnMatrix;`): wgpu's stage
interfaces take only scalars and vectors, so the translator now carries
each column or element as its own varying and copies them out of and
into a private global around the pack's `main`.
- Switch options defined in an included file whose name is not a shader
extension (`#include "shaders.settings"`) were not read, so every
`program.<name>.enabled` that tested them was false and the composites
were skipped (OQ-13).
- `fogStart`/`fogEnd` mixed the start of one of vanilla's two fogs with
the end of the other, so on clear days fog began at the camera and
Sildurs' water turned white. They now come as the pair of the fog that
is complete nearer (OQ-36).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clean-room (spec-yungsapi.md §2, §3, §5; no mod source read; the legacy
files were deleted unread). horizon-port-pack's new `yung` module carries
`yungsapi:yung_jigsaw` structures and `yungsapi:yung_single_element` pool
elements as the mods write them, so players' packs reach the YUNG draws of
the jigsaw assembler. Fields whose behaviour the specification leaves open
(adapt_noise, max_y/min_y, enhanced terrain, element conditions, modifiers,
priorities, depths, ignore_bounds) are counted as lost by name. YUNG's
other structure, placement and processor types go to the general code
(unsupported or dropped) until the specification covers them.
Deleted: legacy_structure_mods/yung.rs, horizon-world's structure/yung.rs,
the `horizon:yungsapi` keys and `horizon:disables`, which nothing writes
now. Two test initializers of the removed field (enhanced_terrain.rs,
piece_limited.rs) were removed by line from the compiler's error positions.
The layout harness now carries the mods through `horizon-port-pack carry`:
82 of 84 Better Dungeons starts match the oracle piece for piece, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A program sampled every level of a buffer that has mipmaps, even after a
later program had rewritten the buffer: the levels below the first were
stale. Complementary's FXAA, which samples its colour buffer with implicit
derivatives inside branches, picked those levels at edges and drew black
dots along every edge. A buffer now binds its whole chain only while the
chain matches its content, and its first level otherwise (6.1.7).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Custom uniform expressions and programs now see the biome at the camera:
`biome`, `biome_category`, `biome_precipitation`, `temperature` and
`rainfall`, and the `BIOME_*`, `CAT_*` and `PPT_*` constants (spec 3.4).
Before, `biome` and every `BIOME_*` read 0, so a test such as
`biome == BIOME_PALE_GARDEN` held everywhere: Complementary drew the pale
garden's fog over every world, and BSL treated every biome as cold, dry,
swampy and so on at once.
OQ-11: `biome` is the index in the game's biome registry (vanilla's
biomes sorted by identifier); names from before the 1.18 renames map to
their successors; a `BIOME_` name that matches no biome reads -1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Flat terrain's vertex stage reads its quad's first three corners from the
arena page (bound per page at set 3, beside mc_Entity's table) and gives
gl_Normal / vaNormal the face normal and at_tangent the tangent along
the texture's u, with w the handedness of bitangent = cross(tangent,
normal) (spec 5.1). The pack's terrain and water pipelines cull back
faces as the game's own terrain does (clockwise front faces, rows being
flipped to GL's): water's surface no longer shows its downward copy.
Flat draws call a hook per arena page (Flat::draw_with); the arena lists
its page buffers.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
5.1; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
block.properties (spec 4.1) is read: `block.<id>=` names with optional
namespaces and state filters; the most specific matching entry wins.
Terrain vertices carry their sprite layer, not their block, so the game
builds a table of the block sprite layers: each takes the id of the block
named like its sprite, else the id most blocks drawing it share; y is 1
on the water and lava sprites. Flat terrain reads it from a storage
buffer at set 3; other draws and packs without the file read (-1, -1).
Building the table takes 14 ms for BSL, 39 ms for Complementary.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
4.1, 5.1, 5.3; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The terrain vertex arena's pages also get STORAGE usage on devices that
run flat draws (vertex storage and compute), so a shader pack's terrain
program can read the other corners of a vertex's quad to give packs a
face normal and tangent. Horizon's own pipelines are unchanged; the A/B
in the PR shows no frame-time change without a pack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dialog's text inputs are edit boxes (labelled above, CommonLayouts.
labeledElement; width, max length and initial value from the dialog) and
its boolean inputs are checkboxes with their label. A click focuses a box
or toggles a box, Tab and Shift+Tab move between the text inputs, the
keys, shortcuts and typed text go to the focused one. What they hold
fills the command templates and custom actions' payloads.
With this, AuthMe's register and login dialogs work: players type their
password and join instead of being kicked after 30 s.
horizon-ctl's `type` types into the focused input of an open dialog (the
first one when none is), in configuration too. server_e2e's dialog check
now types a number and ticks a box, and checks the command the template
makes of them and the payload of a custom action.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
gbuffers_skytextured (or gbuffers_textured, gbuffers_basic) draws the
frame's sun and moon quads, with their render stages, `texture` reading
the shared atlas their sprites live in. The sky's parts draw in vanilla's
order (disc, sunset, sun, moon, stars, void), in one render pass while
consecutive programs write the same buffers. The sun and moon add to the
sky (SRC_ALPHA, ONE) unless blend.<program> says otherwise. Horizon's
world pass skips its own sun and moon when the pack draws them; a part
without a program stays Horizon's.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
2.3, 5.4; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pack's gbuffers_skybasic (or gbuffers_basic) draws vanilla's sky
geometry, as Horizon's own sky builds it: the disc at y 16, the sunset
fan while the sun is low, the stars and the void disc, each with its
render stage. One static vertex buffer; the frame's colours and rotations
sit in a small uniform the vertex inputs read. The sky draws after the
world copy and before terrain, at the far plane: only where nothing else
drew, without writing depth. It blends with the translucent default unless
blend.<program> says otherwise. Horizon's world pass then skips its own
sky disc, stars and void. The sun and moon stay Horizon's for now
(gbuffers_skytextured is next): the pack's sky covers them.
load_post takes the game's inputs per geometry (GameInputs);
GbuffersInputs gains statements run after the pack's main.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
2.1, 2.3, 5.4; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's open_screen title is a text component, and container screens
draw it as AbstractContainerScreen does: in the label grey, under the
component's own colours, bold, italics and fonts, with the § codes a
literal holds. Horizon flattened it to a plain string, so a plugin's
gold and bold "Server Selector" menu showed in plain grey (#1645).
Chests, hoppers, dispensers, droppers, brewing stands and the other
workstations now draw the server's title. Before, the workstations
ignored it and showed their default name.
horizon-ctl's `screen` reports the title span by span (text, colour,
bold, italic). server_e2e's new `titles` check names a chest on the
vanilla server with a styled CustomName, opens it, and checks the title.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's show_dialog (configuration and play) opens vanilla's
DialogScreen instead of being dropped: the header with the title and the
warning button, the message bodies in a scrolling column, the action
buttons packed into columns (MultiButtonDialogScreen) or side by side in
the footer (notice, confirmation). A button runs its click event: a
custom action goes back as custom_click_action, a command runs (in play;
in configuration vanilla only warns), show_dialog opens another dialog,
copy_to_clipboard copies. after_action closes the screen, leaves it, or
opens the WaitingForResponseScreen. The warning button opens the
"custom screen" warning, whose Disconnect leaves the server. clear_dialog
closes it, and so do the level loading screen and a disconnect. The world
and HUD blur behind it as behind any in-game screen.
Inputs come next; here they are not drawn and keep their initial values.
tools/server_e2e.py is a new harness: a vanilla 26.3 dedicated server and
one Horizon client. Its dialog check shows a dialog with /dialog show,
clicks a command button (the server's trigger), a custom action (the
server's debug log shows its payload) and a wait_for_response one, and
clears a dialog with /dialog clear.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A gbuffers program's renderStage, entityId, blockEntityId and entityColor
(spec 5.3, updated per draw) move to a block of their own at set 2 that
the game picks per draw with a dynamic offset: one slot per render stage.
Terrain draws as TERRAIN_SOLID, translucent terrain as
TERRAIN_TRANSLUCENT. The terrain test pack paints any other stage red.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
5.3, 5.4; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`const bool <buffer>MipmapEnabled = true;` in a deferred, composite or
final program's fragment stage now regenerates that buffer's mip chain
right before the program (6.1.7), on the side programs read, and only
when the buffer changed since its last chain this frame. Each level is
the average of the 2x2 texels above it; levels attach one at a time and
programs sample all of them. Unfilterable buffers (integer, 32-bit
float) get none, with a note.
BSL's bloom read every tap from the full-size buffer before; with its
chain composite4 drops from 0.38 to 0.29 ms at 720p on the Mac mini
(HORIZON_SHADER_PACK_TIMING, the chains included), and blurs as intended.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
section 6.1.7; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
show_dialog and clear_dialog in configuration and play, and
custom_click_action, the answer, in both. Dialog.DIRECT_CODEC as
horizon_client::dialog: the five dialog types, their bodies, inputs and
actions with vanilla's defaults, the values inputs hold, and
Action.createAction (a command template's $(key) substitution with
escapeWithoutQuotes, a custom action's payload of every input's tag).
Nothing uses them yet: the screen comes next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HORIZON_SHADER_PACK_TIMING=1 puts timestamps around every pass of a
shader pack, one frame in 30, and logs each pass's median every 30
timed frames. The frame after a timed one waits for the GPU before
reading them (Metal resolves timestamps without waiting for the passes
that write them). Passes overlap on tiled GPUs, so a pass is charged
from the later of its beginning and the previous pass's end.
This is what attributes a pack's cost to its own programs or to ours
when the machine is shared and FPS comparisons are noisy.
Clean-room: Horizon's own tooling, no spec section; no other loader's
code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a pack whose gbuffers_water (or a fallback down gbuffers_terrain's
chain) loads, translucent terrain (water, stained glass) is drawn by it
after the deferred programs and before the composites, in place, sorted
back to front as the game sorts it, and Horizon's own translucent
terrain draw is skipped.
- blend.<program> (off, or four GL factors); translucent terrain
defaults to SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ONE_MINUS_SRC_ALPHA, and
only blendable buffer formats blend.
- depthtex1/depthtex2: depth without translucent terrain, a copy taken
just before it when a later program reads them (before it, depthtex0
itself). The hand is drawn after the pack, so depthtex2 = depthtex1.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 2.1, 2.3, 3.2 (blend), 6.2 (depthtex1/2); no other loader's
code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- RGBA16 and R11F_G11F_B10F buffers get their own formats where the device has them
(16-bit normalized render targets need the adapter's format features): the float stand-in
kept NaN and values past 1, and BSL's TAA history went black for good.
- Textures share one sampler variable per kind (linear, nearest, repeat, nearest-repeat,
comparison): devices have as few as 16 sampler slots, Complementary's terrain program
declares 23 textures.
- A gbuffers pass reading the depth buffer or a buffer it draws into reads a blank texture
instead (a feedback loop: undefined in GL, refused by wgpu, and a crash here).
- A bind group layout that fails validation skips the program with the error's details
instead of ending the game.
- shadowModelView / shadowProjection (and inverses) for programs that read them before the
shadow pass exists: an orthographic camera 160 blocks each side, toward the shadow light.
- HORIZON_SHADER_PACK_SKIP=a,b leaves programs out (a developers' aid to bisect a pack).
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, sections 6.1, 6.7,
5.2; no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`uniform.<type>.<name>` and `variable.<type>.<name>` of shaders.properties (3.4): an
expression parser with C precedence, the function table (math, vectors, `if`, `between`,
`equals`, `smooth`, `random`...), evaluated on the CPU once per frame in file order after the
frame's own uniforms. `smooth` keeps its state per call site and halves the distance to its
target every fade time (OQ-26 working answer). A name neither the frame nor the pack defines
reads 0 and is listed once in the log.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, section 3.4; no other
loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
naga keeps only constant initializers of globals, so every initialized global that is not
`const` (and every conditional one) gets its value from a function of ours that main calls
first, one per declarator, converting as GLSL's initialization does (`vec2 v = atlasSize`).
`vec3 a = x, b = y;` no longer loses `b`.
Program-enable expressions (3.3) now see the pack's switch options that are on by default
(8.1): `#define NAME` with nothing after it, tested with `#ifdef`/`#ifndef` anywhere in the
pack (OQ-13 working answer). BSL's `deferred` (ambient occlusion) and its TAA ran with options
off before.
Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, sections 3.3, 8.1;
no other loader's code read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PrimedTnt keeps Entity's NO_OP InterpolationHandler and ticks on a client
too: a position packet puts it there at once, its own tick moves it on
(gravity, collisions, bounces, drag) and burns the fuse down, and a client
discards it when the fuse runs out. Horizon interpolated it towards each
update instead, so it trailed about two ticks behind the server.
lan_e2e's falling check now drops primed TNT too and checks that the
guest has it exactly where its fall from rest puts it for the fuse the
server sent.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a pack whose gbuffers_terrain (or a fallback) loads, solid and
cutout terrain is drawn by it into the pack's buffers, in place, between
prepare and deferred, and Horizon's own solid terrain draw is skipped.
- depthtex0 is now a Depth32Float attachment: the blit writes the
world's depth there (GL window depth, GL rows), terrain tests and
writes against it, so entities and terrain occlude each other; later
programs sample it as before.
- The vertex glue reads Horizon's TerrainVertex on both draw paths
(instanced, and flat with the page tables in bind group 1) and gives
atlas coordinates, lightmap coordinates in 0-240, the vertex colour,
mc_midTexCoord at the sprite's centre and mc_Entity = -1. Normals,
tangents and block ids come later.
- texture/gtexture/tex sample the block atlas, lightmap the lightmap;
atlasSize is set.
- Occlusion culling is off while a pack draws terrain (it reads the
depth of Horizon's terrain draw), and flat draws stay on.
- Uniforms a program reads that are not provided yet are reported.
- Program building is shared by quads and terrain (build_pass, bind).
- terrain-test: Horizon's own pack for this slice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wasmtime 36 read WebAssembly with wasm-tools 0.236, wit-component (cargo
horizon-mod, the sandbox tests) used 0.239 and the SDK's wit-bindgen 0.247:
three copies of wasmparser, wasm-encoder, wit-parser and wasm-metadata.
wasmtime 48, the next long-term-support line (fixes until August 2028),
wit-component 0.254 and wit-bindgen 0.60 all use 0.254. No code change;
horizon-mods now needs Rust 1.95.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:25:10 +02:00
823 changed files with 62358 additions and 26238 deletions
@@ -34,7 +34,9 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~190 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-client` | The game: a lib (`src/lib.rs`, ~180 flat modules) 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-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 |
@@ -156,7 +159,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
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`). Most harnesses test this path.
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.
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:
@@ -164,8 +167,8 @@ 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 in-frame, guest is a server client
python3 tools/lan_e2e.py --server-thread --check <name> # host on its server thread (portals, levels, distance, lava need it)
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
@@ -4,8 +4,8 @@ Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, a
everything is that **it plays exactly like vanilla**. This page is the short version of how we work; questions are
welcome on [Discord](https://discord.gg/HcTeMZxe5E).
This repository holds the game, the launcher and the website, plus the tools, tests and docs that build, check and
release them. Anything else (videos, Discord and other community material) lives elsewhere.
This repository holds the game and the launcher, plus the tools, tests and docs that build, check and release them.
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything else (videos, Discord and other community material) lives elsewhere.
**Where to start: [playhorizon.dev/hub](https://playhorizon.dev/hub).** It shows what is being done now and next
([ROADMAP.md](ROADMAP.md) in the repository), the work board, good first tasks, and the forms to report a bug or
@@ -31,7 +31,7 @@ Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that re
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 JDK, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 runtime, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
@@ -134,7 +134,7 @@ Better tools for mod makers: a test runner that needs no game window, templates
`cargo horizon-mod test` runs a mod's test scripts against a fake game snapshot ([#1816](https://github.com/ludovic111/horizon/issues/1816)) and, for server mods, scripted server events ([#1837](https://github.com/ludovic111/horizon/issues/1837)); the content template and the guides are merged ([#1859](https://github.com/ludovic111/horizon/issues/1859), [#1810](https://github.com/ludovic111/horizon/issues/1810), [docs/api/stability.md](docs/api/stability.md)). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference entry is removed without a new major version, and the SDK on crates.io once the API is stable.
@@ -160,6 +160,66 @@ Translatable components resolved through the selected language for items, contai
</details>
#### Cooldowns drawn on items
After you throw an ender pearl, its slot should show the cooldown sweeping away, as in vanilla.
<details><summary>Technical</summary>
Vanilla draws `ItemCooldowns`' percent as a white overlay over the slot (`GuiGraphicsExtractor.itemDecorations` and `itemCooldown`) in the hotbar and in inventories. Issue [#1763](https://github.com/ludovic111/horizon/issues/1763).
The Hidden and Commands Only chat settings should hide chat, and team prefixes should appear before names in chat.
<details><summary>Technical</summary>
Vanilla checks `ChatVisiblity` before it delivers chat and system messages ([#1376](https://github.com/ludovic111/horizon/issues/1376)), and chat uses the team-formatted display name ([#1378](https://github.com/ludovic111/horizon/issues/1378)). Both issues have exact steps to reproduce.
The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3.
<details><summary>Technical</summary>
26.3's `AbstractWidget` asks for a cursor from `CursorTypes` when it is hovered; winit's `CursorIcon::Pointer` gives the same shape. Issue [#1347](https://github.com/ludovic111/horizon/issues/1347).
Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable.
<details><summary>Technical</summary>
`EditBox` deletes by word with Ctrl ([#1764](https://github.com/ludovic111/horizon/issues/1764)); the Game Rules and Statistics lists' scroll bars can't be dragged ([#1782](https://github.com/ludovic111/horizon/issues/1782), after [#978](https://github.com/ludovic111/horizon/issues/978)).
Chickens should flap their wings while they fall, not stand still in the air.
<details><summary>Technical</summary>
Vanilla's `Chicken.aiStep` updates `flap` and `flapSpeed` when the chicken isn't on the ground, and the model's wings follow them. Issue [#1706](https://github.com/ludovic111/horizon/issues/1706).
@@ -250,7 +310,7 @@ Horizon should run well on small machines: integrated graphics, the Steam Deck,
Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far Detail Reduced for integrated GPUs and animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported on the Benchmarks page.
@@ -540,7 +600,9 @@ You can port a mod to Horizon with your own AI agent on your own computer. Horiz
<details><summary>Technical</summary>
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide ([website/docs/agents.md](website/docs/agents.md), [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide (playhorizon.dev/docs/agents, [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
@@ -876,7 +938,9 @@ Run Horizon's built-in benchmark and share the report: every new computer makes
<details><summary>Technical</summary>
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it ([website/docs/performance.md](website/docs/performance.md)). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it (playhorizon.dev/docs/performance). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
@@ -902,56 +966,6 @@ Armour and tools in End city chests should come enchanted, as in vanilla.
</details>
#### Cooldowns drawn on items
After you throw an ender pearl, its slot should show the cooldown sweeping away, as in vanilla.
<details><summary>Technical</summary>
Vanilla draws `ItemCooldowns`' percent as a white overlay over the slot (`GuiGraphicsExtractor.itemDecorations` and `itemCooldown`) in the hotbar and in inventories. Issue [#1763](https://github.com/ludovic111/horizon/issues/1763).
</details>
#### Chat settings and team prefixes
The Hidden and Commands Only chat settings should hide chat, and team prefixes should appear before names in chat.
<details><summary>Technical</summary>
Vanilla checks `ChatVisiblity` before it delivers chat and system messages ([#1376](https://github.com/ludovic111/horizon/issues/1376)), and chat uses the team-formatted display name ([#1378](https://github.com/ludovic111/horizon/issues/1378)). Both issues have exact steps to reproduce.
</details>
#### A pointing hand over buttons
The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3.
<details><summary>Technical</summary>
26.3's `AbstractWidget` asks for a cursor from `CursorTypes` when it is hovered; winit's `CursorIcon::Pointer` gives the same shape. Issue [#1347](https://github.com/ludovic111/horizon/issues/1347).
</details>
#### Ctrl+Backspace and scroll bars in menus
Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable.
<details><summary>Technical</summary>
`EditBox` deletes by word with Ctrl ([#1764](https://github.com/ludovic111/horizon/issues/1764)); the Game Rules and Statistics lists' scroll bars can't be dragged ([#1782](https://github.com/ludovic111/horizon/issues/1782), after [#978](https://github.com/ludovic111/horizon/issues/978)).
</details>
#### Chickens flap while they fall
Chickens should flap their wings while they fall, not stand still in the air.
<details><summary>Technical</summary>
Vanilla's `Chicken.aiStep` updates `flap` and `flapSpeed` when the chicken isn't on the ground, and the model's wings follow them. Issue [#1706](https://github.com/ludovic111/horizon/issues/1706).
</details>
## Recently shipped
- **OpenGL for old computers** (v0.2.63): Horizon now starts on old PCs whose graphics card has no Vulkan or DirectX 12 driver: it switches to OpenGL by itself, and Options > Graphics can pick OpenGL by hand. [#1917](https://github.com/ludovic111/horizon/issues/1917) · [#1920](https://github.com/ludovic111/horizon/issues/1920) · [#1936](https://github.com/ludovic111/horizon/issues/1936)
description="The client's lowest layer: chat text and translations, SNBT and NBT paths, the scoreboard, boss bars, game rules and the registry and particle tables commands name"
other=>returnErr(format!("command harness: unknown op {other}").into()),
}
Loaded 100 of 823 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.