A developer of Iris showed that Horizon's shader pack code was written from
Iris's source. This removes it: the horizon-shaderpack crate, Java pack
loading and its render stages, Horizon's own WGSL packs and the example
packs, the Shader Packs screen and its option, the launcher's shader pack
browsing and installing, and the docs about them. Shader packs may come
back later as a clean-room rewrite.
The vanilla renderer and its post-processing (resolution scale, FXAA) are
unchanged. Options files that name a pack load and drop the key; the
launcher reads manifests that list a shader pack, and files in
`shaderpacks/` are never deleted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher had no app menu and no key bindings, so Cmd+Q did nothing.
Add Quit and CloseWindow actions, bind them to cmd-q and cmd-w on macOS,
and set an app menu with "Quit Horizon Launcher" and a Window menu with
"Close Window". Quit shares one shut_down path with closing the window
(stop the servers this launcher started, send telemetry, exit), so a
running game is treated exactly as it is today.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Walking through the cart pushes it back once past its middle, so the
check stands the player half into the cart from the north and waits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One release per merge made about 40 releases in 31 hours, hard for players to follow (asked on Discord).
release-now still releases at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lan_e2e's vehicles check: a creative break whose block is put back stays,
a player stands on a boat, lava breaks a ridden boat and lets the rider
out, a minecart in lava breaks and a TNT one goes off, and walking into a
minecart pushes it, for the host and the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a LAN guest or any remote client, getblock and container read what
that client has again (test_friends waits on a guest's getblock). On the
server thread the block commands run in the owner's level, not whichever
level the server ticked last.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractBoat.tick runs applyEffectsFromBlocks twice, and a minecart takes
lavaHurt from its behaviour's applyEffectsFromBlocks and again from its
own isInLava check: two 40-point hurts a tick, so lava breaks either on
its first tick inside (a TNT minecart primes instead). A minecart has no
baseTick and a boat never outlives lava, so the fire countdown is gone.
Minecart pushing takes the player's own box and returns at once when
there are no carts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Guests on the server thread (and LAN) now push carts with vanilla's push
math. The push back on the player is dropped in both modes: vanilla only
syncs it to other players, so the pusher's own client never feels it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vehicles take lava damage and keep burning for 15 seconds like vanilla
(lavaHurt, remainingFireTicks), breaking and dropping their items; a TNT
minecart primes from fire. Boat placement stops at lava as well as water.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Boats join happy ghasts in the player's entity collision list, so a
player can't walk through a boat and steps up onto it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the integrated server thread on, setblock, fill, getblock and
container ran on the client's copy only. They now run on the server, and
on a remote server setblock and fill go out as chat commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A creative START destroys the block at once and its client never sends
STOP, so the kept dig broke whatever was placed in that cell next, every
destroy delay, until the player broke something else.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.causeExtraKnockback pushes along Mth.sin/Mth.cos of the attacker's
yaw, which come from vanilla's 65536-entry table: facing east, the push
has no z at all. Horizon took f32 sin/cos, which left -4.4e-8 of z per
block of push (a sprinting or Knockback hit, on mobs and players).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.causeExtraKnockback stops the attacker's sprint on the server, and
the shared flag reaches the attacker's own client (ServerEntity sends a
player its own dirty entity data). A guest's sprint was only stopped on
the server copy for the length of the hit and never sent, so a guest (and,
on the server thread, the window) kept sprinting: every hit while
sprinting had the sprint knockback, where vanilla's needs the sprint to
start again.
- leave_guest keeps the sprint stop a guest's hit made; a guest hitting a
player stops its sprint too.
- The hub sends each guest its own shared flags (crouching, sprinting,
gliding) when they change.
- A remote client takes the sprinting bit of its own shared flags (the
flag only, as vanilla's assignValues: the speed modifier and the
wasSprinting it compares stay, so it sends START_SPRINTING again only
after it has stopped).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's (and, on the server thread, the window's) knockback started from
the velocity the last hit left on its server copy, which nothing ever
slowed down, and each hit went out at once. So every hit after the first
pushed harder (0.72, then 0.76 blocks a tick where vanilla sends 0.4 each
time), netherite resisted from the wrong base, and a zombie's hit reached
the client before the friction of the tick it landed in.
Vanilla's server simulates a player too (MoveSimulationType
AUTHORITATIVE_SIDE_AND_SERVER): ServerPlayer travels every tick with no
input, and ServerEntity.sendChanges sends a velocity a hurt marked
(Entity.markHurt) as it is after that travel. A player's melee hit is the
exception (Player.causeExtraKnockback): it sends at once, then puts the
server copy back to its velocity from before the hit.
- MotionWorld::server_travel: Player.travel with no input, keeping only
the velocity (the client's moves place the player). The server's copy
checks no fall damage on its own moves (isLocalInstanceAuthoritative).
- guest_server_tick runs it last, then sends the velocity if a hurt
marked it since the last tick.
- guest_hit_by leaves a marked velocity to that; guest_struck_by (melee
from a player) sends at once and restores.
- lan_e2e `knockback`: three hits each way push the victim as far each
time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stable Clippy (1.99, what CI runs) does not flag q < -2. || q > 2., so the
helper and its test only served Rust 1.90's Clippy. Fire burnout is not
'intentionally disabled': vanilla's FireBlock.tick reads the
INCREASED_FIRE_BURNOUT environment attribute, set by eight overworld biomes,
which Horizon does not read yet; the comment now says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI runs stable Clippy (1.99), which is clean on main. The #[expect(clippy::boxed_local)]
in streaming.rs is unfulfilled there and fails CI under -D warnings, so it goes.
Also back out the rewrites that only satisfied Rust 1.90's Clippy and read
worse: the dragon's range helper and its test, the axolotl test's restated
formula, and two De Morgan expansions (first_person_probe, explosion).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 multiplies bobHurt/bobView into the projection
(GameRenderer.renderLevel); Iris's MixinModelViewBobbing moves them into
the model-view when a pack is in use. The comment said vanilla keeps the
bob in the PoseStack, which is Iris's behaviour, not vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/test_server_knockback_parity.py runs vanilla's dedicated server and
Horizon's (`--dedicated --nogui`) on one flat world and joins both with two
vanilla-codec players (KnockbackClient):
- Alice hits a cow and a zombie (plain, sprinting, Knockback II, critical,
in the air); the game is frozen around the hit and stepped a tick at a
time, and the mob's Motion and Pos over 20 ticks are compared exactly;
- Alice hits Bob (the same, a shield, netherite, two hits, two sprinting
hits, a mace smash, a stronger hit inside the hurt cooldown): Bob's own
motion packets, Alice's and her sprint flag are compared;
- a zombie hits Bob twice: his motion packets.
7 of 21 cases match on main. KNOWN lists the other 14 with their causes:
the guest's server-side velocity never travels (knockback adds up from hit
to hit), the server never sends a guest its own sprint flag, the extra
knockback's direction uses f32 sin/cos instead of Mth's table, a hit in
the hurt cooldown still knocks back, and a mace smash on a player runs
none of MaceItem.hurtEnemy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hit that lands while the player is still in its hurt cooldown, harder
than the last one, only hurts by the difference (LivingEntity.hurtServer):
it marks nothing and deals no default knockback. Horizon knocked the
player back 0.4 for it anyway, so a quick second hit (a weak swing, then a
charged one) pushed twice.
The hurt now reports whether its knockback hook ran (a fresh hit, not
fully blocked), and mob_hits_player knocks back only then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`/give @s wooden_sword[enchantments={knockback:2}]` (or an item saved
that way) kept the key `knockback`, which no enchantment lookup matched:
the sword showed no enchantment and knocked back like a plain one.
Vanilla reads the keys as Identifiers, so `knockback` is
`minecraft:knockback`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack rebuilds view positions from the depth buffer with
`inverse_projection` and then multiplies by `inverse_view`
(`gbufferModelViewInverse`), and Horizon's own packs do the same through
`world_offset` and `frame.inverse_view`. Passing
`world_projection(fov, aspect, far) * bob` as the projection while `view`
stayed unbobbed left the two halves disagreeing by the walk bob, so
everything a pack reconstructed — reflections, ambient occlusion,
godrays — turned with the bob. That reads as reflections wobbling in
step with the first-person hand, and changing the FOV made it worse.
Vanilla and Iris carry the bob in the world's pose stack (the model-view
matrix), not in the projection. Pass `bob * view` as the view and the
plain world projection instead: `projection * view` is unchanged, so
terrain, entities and the pack's own geometry programs draw as before,
while depth reconstruction is now consistent. The hand's uniforms
already carried the bob in its pose and are untouched.
Checked with Complementary Reimagined r5.9.3 over the depth-feedback
passes at 70 and 110 FOV.
Co-Authored-By: DeepSeek V4.1 Flash (OpenCode agent) <[email protected]>
horizon-client, horizon-world and horizon-protocol each had an example
named `probe`. Cargo copies every crate's examples into one
<profile>/examples/ directory, so parallel parity harnesses sharing a
target dir could run another crate's probe (CI parity run 40055656303:
command_parse ran the horizon-world dispatcher). The examples are now
client_probe, world_probe and protocol_probe ([[example]] in each
Cargo.toml; the files stay where they are), and every harness builds
and runs its own crate's name.
tools/parity.py recognises the new names when it counts the probes a
harness ran, and renames the old `probe` in recorded artifacts to the
crate's target (from the record's -p, else its probe sources), so the
prebuild still builds them instead of dropping them as unknown.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main. Single player runs on its server thread by default, as vanilla does; `--in-frame` brings back the old mode. Evidence: the smoke session passes 48/48 in both modes. ThinkPad (HD 620, Mesa) RD 16: 651 vs 185 FPS. M4 Pro, v0.2.25 release build, forest, offscreen, alternating runs: 1% lows 224–231 → 399–476 at RD 16 and 120–124 → 210–213 at RD 32; average FPS within 4% at RD 32.
Villager.spawnGolemIfNeeded set GOLEM_DETECTED_RECENTLY on the villagers
near a new golem, but Horizon wrote only villager::Ext, and the next brain
tick copied the empty memory back over it: the same villagers could summon
another golem before their sensor saw the first. golem_detected now sets the
brain memory (599 ticks).
IronGolem.checkSpawnObstruction checks the floor's top face (no golems on
dirt paths, farmland or slabs) and three blocks, the feet whatever fluid is
there; Horizon checked four blocks and no floor, so a roof three blocks up
kept golems out. The search's open block is air or a liquid block, not any
waterlogged one, and the villagers asked are those whose boxes meet the
caller's box grown by 10. The golem is created (yaw, follow range bonus,
left-handedness from the level random) before the obstruction check, as
trySpawnMob does.
The search and checks move to mobs::villager so the mob_ai probe runs the
game's code: four new scenarios (gossip, glass roof, dirt paths, panic) match
vanilla; with the old checks all four diverge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resources prepared before #1538, and every version that reused them, lack vanilla's built-in
packs, so players had to click Prepare again. The launcher now copies them itself, with
horizon-prepare's `packs` (the asset index's SHA-1 checked), when it starts, installs a version
or starts the game: only for prepared versions that lack a zip, once per launcher run. Without
them in any Minecraft folder's asset store it logs one line and the game starts as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (goldendays-1). Prepare copies vanilla's built-in Programmer Art and High Contrast packs from the player's own launcher asset store (SHA-1 checked; nothing ships with Horizon), so packs made to sit on Programmer Art, like Golden Days, show their classic textures.
Vanilla lists its built-in resource packs from minecraft:resourcepacks of
the vanilla pack (BuiltInPackSource.populatePackList), and those two zips
live only in the asset index, not in the client JAR. Preparation never
copied them, so Horizon's pack list had no Programmer Art. Packs made to sit
on it, like Golden Days ("This pack is meant to be put over Programmer
Art"), then showed the modern textures for the ~720 textures they leave to
it: furnaces, stone bricks, crafting tables, pumpkins, TNT, logs, tools,
buckets, paper.
Full preparation now imports both after extracting the JAR, like the
panorama and Unifont, and `horizon-prepare packs` adds them to prepared
resources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
PathNavigation.setRequiredPathLength calls updatePathfinderMaxVisitedNodes, so a
villager's PathFinder visits up to max(follow range, 48) * 16 = 768 nodes, not the
256 of its follow range alone. Horizon always used follow range * 16, so a villager
gave up on any job site, bed or bell whose path needed more than 256 nodes: in the
seed 12345 village it never found the way round to the mason's stonecutter door.
Navigation now keeps PathFinder.maxVisitedNodes: the base follow range times 16 when
the mob is made, then max(follow range, required path length) * 16 after
setRequiredPathLength and after any follow range change (Mob.onAttributeUpdated).
A happy ghast that grows up or becomes a baby gets a new navigation, as in vanilla.
mob_ai: villager_job_in_box, a composter inside a stone box whose doorway faces away
from the villager.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (discord-gate-1: player reports checked against vanilla 26.3; a lan_e2e check fails on main and passes here in both modes; evidence in the PR body).
InteractWithDoor was a stub, so a villager whose path went through a
closed oak door walked into it and stayed there. In generated villages,
villagers who were inside a house never reached a job site, a bed or
the meeting point.
Like vanilla, a villager now opens the closed mob-interactable doors on
its next path step (DoorBlock.setOpen: both halves, the door sound and
the block_open game event), remembers the door it leaves and the doors
it opened, and closes them once it is more than one step past them,
unless another villager within 2 blocks is walking through. A villager
going to sleep closes them too.
The mob events apply after the mob loop, so the door opens one tick
later than in vanilla, where DoorBlock.setOpen changes the world during
the villager's brain tick.
lan_e2e's new doors check shuts a villager in a room with a closed
door and a free composter outside. Before, it never opened the door;
now it opens it, becomes a farmer, and the door is closed again on the
server and on the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-2: server-thread gaps checked against vanilla, each with a lan_e2e check that fails before and passes after, host and guest, both modes; numbers in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
🤖 Agent: merged after verify and CI parity passed, built merged with current main. Code mods become horizon-client's `mods` feature: CI's two test commands run with `--features horizon-client/mods,horizon-mods/pack`; tests pass with and without it.
🤖 Agent: merged after verify and CI parity passed, built merged with current main (launcher-ship-1). #1521: generated docs keep LF line endings on Windows, so the launcher builds there again (dry run 40035909909 green on all three OSes). #1522: Prepare no longer requires Netty's optional native transport jar, which Prism never downloads (fixes#1487; test plus a real Prism-layout prepare).
🤖 Agent: merged after verify and CI parity passed, built merged with current main. The dedicated_server flake was the harness client never resending its position, so a villager whose chunk went out after its only move was never shown to it (0/21 → 21/21 with the server's tick thread stalled after the join). Failing harnesses now print the reason in CI.
🤖 Agent: merged after verify and CI parity passed, built merged with current main (reports-gate-2: each claim checked against vanilla 26.3, in-frame and --server-thread; evidence on the issue and in the PR body).
ServerPlayerGameMode breaks a digging player's block only when the
player's client says it is done (STOP_DESTROY_BLOCK, or the delayed
destroy), or at once when it is instant. The host ran the guest's
tick_mining with its own count and broke the block when that reached 1.
A guest whose client ran even a tick behind lost the block before it
predicted the break: no destroy particles, no break sound, and the
dropped item could appear before the block vanished.
lan_e2e's new digging check holds attack across six oak logs and hears
six breaks, then stalls the guest for a second halfway through stone:
the stone is still there on both sides when it resumes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st2: entity use with either hand and server chat components; lan_e2e checks pass in both modes. The hands check's only flake was a vanilla crit arrow killing the cow, fixed in c89695dd9: 40 of 40 runs pass).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (launcher-ship-1). #1521: generated docs keep LF line endings on Windows, so the launcher builds there again (dry run 40035909909 green on all three OSes). #1522: Prepare no longer requires Netty's optional native transport jar, which Prism never downloads (fixes#1487; test plus a real Prism-layout prepare).
The harness failed about 1 run in 3 on CI with "play-trade villager false" on
Horizon's side: the protocol client never saw the villager it trades with.
A server pairs a player with an entity only while the entity's chunk has been
sent to it, and checks again when either moves (ChunkMap.move / updatePlayers).
VanillaProtocolClient sent its position once, at the spawn teleport, and never
again. Horizon's first chunk batch goes out two ticks after the player is placed
(after the first tick's packets, as vanilla orders them); when a busy machine
held the tick thread back long enough for the client's move to be handled one
tick after placement, the villager's chunk wasn't sent yet, and with no later
move the villager was never paired.
The 26.3 client re-sends its position every 20 ticks standing still
(LocalPlayer.sendPosition, after a ServerboundClientTickEnd each tick). The
parity client now does the same from the time it is loaded until its death, so
an entity whose chunk comes late is paired at the next reminder, on either
server.
On failure the harness now adds the servers' and clients' warnings and errors to
its message and the client log's tail when the client fails, and parity.py's
summary prints the end of the first failing harnesses' logs, so CI shows why.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher.yml's Windows build has failed since the reference test landed: Git for Windows checks
text out with CRLF (core.autocrlf), so docs/launcher-commands.md never equalled what markdown()
generates. .gitattributes now pins LF for the generated files tests compare byte for byte, and
the launcher's check reads the file whatever line endings a clone gave it, with a test of a
CRLF checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Prism Launcher never downloads io.netty:netty-transport-native-kqueue (or the epoll one on
Linux), so preparing from a Prism install stopped on a jar the game runs without: vanilla's
EventLoopGroupHolder.remote takes KQueue/Epoll only when isAvailable(), else Netty NIO.
Preparing now imports a native transport when the launcher has it (checked against the
version JSON's sha1, as before) and goes on without it otherwise; every other library stays
required. Fixes#1487.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (reports-gate-2: each claim checked against vanilla 26.3, in-frame and --server-thread; evidence on the issue and in the PR body).
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st2: entity use with either hand and server chat components; lan_e2e checks pass in both modes. The hands check's only flake was a vanilla crit arrow killing the cow, fixed in c89695dd9: 40 of 40 runs pass).
#1428 made the in-frame world the default while a server's players lagged
behind it (signs, books, totems, keep_inventory and the other [server-thread]
issues). With the fixes of this stack a scripted single player session (walk,
mine, place, inventory, combat, eating, boats, minecarts, chests, furnaces,
beds, chat, signs, books, totems, keep_inventory, fishing, maps, saving and
reopening) gives the same results on the server thread as in the frame.
`--in-frame`, `--no-server-thread` or HORIZON_SERVER_THREAD=0 runs the world
in the frame; `--server-thread` still works. LAN guests, dedicated servers and
the command harness are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractContainerMenu.removed hands the stack a closing player holds on the
cursor to placeItemBackInInventory (dropped only what doesn't fit, or for a
player who left), and broadcastChanges then empties the client's cursor. The
LAN host threw it out at the player's eyes instead, for the inventory and
every container, crafting, station, mount and merchant menu, and the client
kept showing a copy on its cursor (#1283).
The three ContainerClose arms now place it back through lan_place_back and
send the inventory, carried stack included.
tools/lan_e2e.py gains a cursor check: each player picks up a stack in their
inventory and closes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lightning (Entity.thunderHit) and evoker fangs (EvokerFangs.dealDamageTo) hit
every entity in their box, but thunder_hit and fangs_bite only looked at the
window's own player, so a server's players (guests, and single player on the
server thread) were never struck (#1480, #1481). Both now run their player
part for each guest too, through Game::each_guest_hit.
The effects a mob gives the player it acts on (cave spider and bee poison, husk
hunger, wither skeleton wither, the nautilus's breath) and an axolotl's help
after a kill stayed with the server's own player: acts_on_player now routes
MobEvent::EffectPlayer and MobEvent::AxolotlSupportPlayer to the guest the mob
sees, like its blows (#1484, #1485).
tools/lan_e2e.py gains a strikes check: a bolt hurts each player standing in
water, and a cave spider poisons them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (guests, and single player on the server thread) the window ticks
its player through tick_remote_level, which copied the yaw, the Respiration
level, the wings and FAST_LAVA but not the rest of what the in-frame tick sets
from the window's own state. Both now call Game::sync_player_body:
- the look pitch: Player.travel steers sprint-swimming up and down by it (#1303)
- the slowdown of an item in use, LocalPlayer.modifyInput (#1278)
- leather boots, PowderSnowBlock.canEntityWalkOnPowderSnow (#1296)
- freeze-immune wear, LivingEntity.canFreeze
- the Auto Jump option, LocalPlayer.sendPosition (#1279)
tools/lan_e2e.py gains a body check: each player walks while drawing a bow,
stands on powder snow with leather boots and without, and sprint-swims looking
down.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed. A stopgap while in-frame is the default: an overloaded world slows down instead of dropping to 2 FPS (2,500 pigs: 1.7 → 16 FPS). It is removed with the in-frame path (FEATURES 4zg) once the server thread is the default again (#1463–#1466 and following).
A map a server's player (a LAN guest, or single player's window on its
server thread) made stayed blank and saved nothing (#1382): only the
game's own player ever updated a map, and no map data went to the guests.
Each guest's turn now runs MapItem's ticks as theirs (the marker of every
carried map, the reveal of the held ones), and the server sends each guest
ClientboundMapItemDataPacket for the maps it carries, and every 10 ticks
for the maps framed in the level (ServerPlayer.synchronizeSpecialItemUpdates,
ServerEntity.sendChanges).
As in vanilla, a map keeps its holders (MapItemSavedData.HoldingPlayer):
each has its own column sweep and its own record of what changed since its
last packet, so the packet carries only the changed pixels' box, and the
decorations at most every 5th tick while they changed. Player markers are
kept per holder, so each player's marker shows on a shared map, and go
when that player stops carrying it. A server's client now applies the
packet's decorations.
lan_e2e maps: host and guest each make and hold a map, which draws the
terrain around them and their marker, in both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's players (LAN guests, and single player's window on its server
thread) cast no lasting hook and caught nothing (#1348): the guest's cast
went into the game's own hook, which the server's tick then dropped for
want of a local player. Each guest now keeps its own hook (Player.fishing),
swapped in with the rest of its state for its turns, ticked as theirs every
server tick, and dropped when they die. Reeling in retrieves it on the
server: a catch, the rod's wear, the experience.
The server tracks every hook to its players as a fishing_bobber entity
(FishingHook.getAddEntityPacket: the owner's id; DATA_HOOKED_ENTITY and
DATA_BITING). A client takes the one its own player cast as its hook, so
the rod shows cast and the line runs to its hand, and draws the others'
bobbers on lines to their owners' hands.
A hook resting on a block stays on the ground (Entity.move keeps onGround
when nothing moves vertically); it used to flip every tick, so reeling it
in cost the rod 2 only half the time.
lan_e2e fishing: host and guest, both modes: the hook stays out and both
sides draw it, reeling in from the ground costs 2, and in open water with
Lure III a fish bites and is caught for 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
🤖 Agent: merged after verify passed, with local proof: all 100 client harnesses ran on main and on the branch and their transcripts were diffed (89 byte-identical, the rest only timings or stamps). 81 client examples become one probe binary.
🤖 Agent: merged after verify passed, with local proof: all 217 world harnesses ran on main and on the branch and their transcripts were diffed (203 byte-identical, the rest only timings or known run-to-run counts). 168 world examples become one probe binary, with 1,097 duplicated helper lines and 11 dead examples gone.
A wind charge's burst (ServerExplosion with TRIGGER_BLOCK) pushed only the
game's own player, so a LAN guest, and single player's window on its server
thread, never rose (#1386). The burst now pushes every player in reach, each
swapped in as the player, and sends ClientboundExplodePacket to the players
within 64 blocks with the push it gave them (hitPlayers), which their client
adds (Entity.pushFromExplosion); onExplosionHit's fall-damage grace goes on
the server's body. A burst from a guest's own turn (their charge landing,
their mace's wind burst) waits for the turn to end, so that it reaches the
host and the other guests too.
The packet's centre particle is the gust emitter and it throws no block
particles, as WindCharge.explode asks; the client now draws the packet's
own particle. Explosion knockback resistance lowers the player's push as in
vanilla, and a guest's body knows whether it flies (no push while flying in
creative).
lan_e2e wind: host and guest each throw a charge at their feet, rise about
six blocks and land unhurt, in both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fully drawn bow fires a crit arrow: AbstractArrow.onHitEntity adds
random.nextInt(damage / 2 + 2) to the 6 damage, so one shot in five does 10
and kills the 10 health cow. The off hand bucket then had nothing to milk
("the host's off hand holds minecraft:bucket"), on either side, in any mode.
Under 20 ticks of draw the arrow is no crit and does at most 6. The check
now also asserts the cow is still there after the shot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st1: /particle and explosion particles from a server reach its players; lan_e2e evidence in the PR body).
BundleItem.broadcastChangesOnContainerMenu calls slotsChanged(player
inventory) after a bundle click. The menus that don't check which container
changed recompute from their inputs (crafting grids, loom, stonecutter,
cartography, merchant, smithing's hasRecipeError). The menu click harness
found it on a loom with a fixture result: 8000/8000 cases now match (seeds 7
and 11).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
🤖 Agent: merged after verify passed; main builds again. Local proof on the Mac mini: cargo check -p horizon-client --bins --tests fails on 8a4ebcc55 with the two E0599 errors and passes here.
#1432 deleted both as unused; #1424 (level events) and #1425 (scoreboard)
merged after it and call them, so horizon-client's binary failed with
E0599 at level_particles.rs:54 and scoreboard.rs:338. Both functions come
back unchanged from 4d7f30f14^1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every drawn character looks its glyph up in the font set's cache, and with
std's SipHash the hashing was the top cost of drawing sign text (#1461:
100 text signs in view). FxHash for those char keys cuts the frame's
prepare time with 100 text signs from 0.32 ms to 0.22 ms on the Mac mini
(Metal, 1280x720): 1920 to 2250 fps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shadow quads were lifted 0.001 above the block top and drawn in the entity
batch with depth writes. LivingEntityRenderer's translate(0, -1.501, 0)
puts a model's feet 0.001 above the ground too, so a chicken's feet and the
shadow under it were coplanar and flickered (#1440).
ShadowFeatureRenderer puts the quads on the block top itself
(relativeY + shapeBelow.minY), and RenderPipelines.ENTITY_SHADOW tests depth
without writing it. Shadows are now their own entity material (overlay 14)
drawn by such a pipeline after the entities. A small depth bias, constant
plus 1.5% of the slope, keeps them over the terrain under them, whose
vertices snap to the subpixel grid differently (without it whole shadows
vanished at some camera positions), while staying under feet 0.001 higher.
The shader pack shadow map no longer draws entity shadows as casters.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A carried bundle clicked on a stack took its place instead of taking it in
(#1456): Horizon's menus had no BundleItem click overrides. The PICKUP click
now runs them first, as AbstractContainerMenu.tryItemClickBehaviourOverride:
- a carried bundle takes as much of a clicked stack as its weight allows
(tryTransfer), and a right click on an empty slot puts its first stack
there (removeOne, the rest going back in);
- a stack clicked onto a bundle in a slot goes in as far as it fits, and a
right click with an empty hand takes the bundle's first stack out.
BundleContents.Mutable is in horizon_client::bundle (exact Fraction
weights: 1/max stack, 1 for a beehive with bees, a nested bundle's contents
plus 1/16; shulker boxes don't fit), used by the menu engine and the
LAN/server-thread container engine. The insert, insert-fail and remove-one
sounds play at the player.
In single player the inventory screen's empty-hand press went to an older
click path instead of the menu engine; it now goes to the engine, as on a
server, so the right click on a bundle works there too.
The menu click harness now has bundles (empty, loaded, nested; often in
hand) and compares the player's sounds: 3000/3000 bundle-heavy cases match,
2796/3000 without the overrides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreativeModeInventoryScreen.keyPressed on the search tab tries the hotbar
keys on the hovered slot before the search box: a digit or keypad key bound
to key.hotbar.N swaps when the cursor is over a slot holding an item, or any
slot that is not a display slot, and the cursor stack is empty
(checkHotbarKeyPressed). Over an empty display slot, outside every slot or
with a stack held, the key still types. Horizon sent every key to the search
box (#1460).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FirstPersonHandsAndItemsRenderer.submitArmWithItem draws a stack with a
map_id two-handed only from the main hand with an empty off hand; any other
map is renderOneHandedMap in its own hand. Horizon only had the two-handed
case and drew every other held map as its item model (#1459).
MapHold picks the case from the hand renderer's visible items, and
one_handed_map_pose is renderOneHandedMap's transform, checked against
vanilla's PoseStack. renderMap (background, map, decorations) is shared by
both cases. The arms under held maps are still not drawn, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Item.use runs on the client as well (MultiPlayerGameMode.useItem), and
only a use the client started sends RELEASE_USE_ITEM when the button
lets go. A server's client started one for bows and food only, so a
shield kept blocking after release, a charged trident never left the
hand, a loaded crossbow stayed "in use" on the server and never fired,
and a spyglass never scoped. The client now starts what the window's
own Item.use does: a bow or crossbow with ammunition, a trident, a
spyglass, a shield off cooldown, and keeps it until the button lets go.
The server's RELEASE_USE_ITEM let go of a main-hand bow only. It now
runs ServerPlayer.releaseUsingItem with the used item's hand, through
the release the window's own tick shares (bow, trident, spyglass).
A loaded crossbow reached its client without its ammunition: the
network stack gets minecraft:charged_projectiles (registry id 51,
ItemStackTemplate list), and a server's client decodes it.
lan_e2e gets a helduse check, for the host and the guest, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
🤖 Agent: merged after verify and CI parity passed (review-0-survey quick win: dead pub items; the generated version.rs is kept as the generator writes it).
A server's client never took the chest slot's Elytra into its own
movement (player.glider) and never told the server it started gliding,
so on the server thread and as a LAN guest the player just fell. The
client now sets the glider as the window's own tick does and sends
ServerboundPlayerCommandPacket START_FALL_FLYING when a glide starts.
The hub used to accept that command blindly and never clear it. The game
now answers it as handlePlayerCommand does (Player.tryToStartFallFlying,
else stopFallFlying), and each tick runs LivingEntity.updateFallFlying
for a gliding guest: the glide ends once canGlide fails (landed, riding,
flying, levitation, a glider about to break), every 20th tick wears the
Elytra, every 10th is an elytra_glide game event, and a glide that isn't
diving keeps the fall distance at 1 (checkFallDistanceAccumulation).
lan_e2e gets an elytra check, for the host and the guest, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's player (a LAN guest, or single player's window on its server
thread) could neither write in a book and quill nor read a written book:
the server opened the screens in the guest's turn and dropped them (#1366).
As in vanilla, the client opens a book and quill's editor itself when it
uses one (LocalPlayer.openItemGui) and sends ServerboundEditBookPacket
when it saves or signs; the server writes the pages or makes the written
book, signed with the player's name, in that hotbar slot or the off hand.
A written book opens on the client when the server sends
ClientboundOpenBookPacket for its hand (ServerPlayer.openItemGui).
horizon-protocol: OpenBook and EditBook (pages a list of at most 100).
horizon-ctl: book sign presses Sign, then Sign and Close; book done on a
book and quill saves it, as its Done button does.
lan_e2e books: host and guest each write, sign, read and drop a book,
both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sign a server's player placed (a LAN guest, or single player's window on
its server thread) opened no editor: its turn's editor was dropped with
the rest of the guest's screens (#1380). The server now does as
ServerPlayer.openTextEdit: the block, then ClientboundOpenSignEditorPacket,
and the player may write on that sign. The client opens its editor on the
sign it has and sends ServerboundSignUpdatePacket when it closes; the
server takes the lines (formatting codes stripped) unless the sign is
waxed, the player was not the one allowed or went out of reach, and sends
the sign to everyone. The window's own sign now reaches its guests too
(sendBlockUpdated, not only a save).
horizon-protocol: OpenSignEditor and SignUpdate, with SignTextSlot as a
VarInt and the lines as a fixed list of four (26.3's stream codecs).
horizon-ctl: type and key reach an open sign editor; block shows a sign's
lines.
lan_e2e signs: host and guest each place, write and see, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The guest death path (LAN guests, dedicated players and single player's
window on its server thread) gave the experience reward but never ran
Player.dropEquipment, so a player died with keep_inventory off and kept
everything (#1284). Both paths now share one drop_inventory:
destroyVanishingCursedItems, then Inventory.dropAll from the eyes less 0.3.
ServerPlayer.die skips the loot for spectators only; the window's own
death skipped it in creative too, which vanilla doesn't.
lan_e2e keepinv: host and guest, keep_inventory off and on, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server's players (LAN guests, and single player's window on its server
thread) never had a totem ready, and nothing applied one: lethal damage
killed them with the totem still in hand (#1292). Each guest turn now
readies the totem from the guest's hands and, when its hurt set it off,
uses it up at the end of the turn (LivingEntity.checkTotemDeathProtection):
1 health, the item_used stat, the used_totem trigger, the use vibration,
the totem's effects.
Entity event 35 now goes to every client around, the holder's own too,
and a client plays it as ClientPacketListener.handleEntityEvent does: the
totem particles, the sound heard locally (not sent on as a sound packet,
so no client hears it twice) and the holder's rumble.
lan_e2e totem: host and guest, off hand and main hand, both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shadow samplers bind with main's custom textures (texture.* first,
then the shadow map, then the rest); the shadow layers are drawn from the
shadow list on the flat path too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With main's Metal culling: the pack's depth test in OpenGL's layout runs
forced early on Vulkan (fs_box_early_depth) and in the shader on Metal
(fs_box_gl, the pack's depth attached read only and bound), and the flat
path culls too (phase 1 calls, flatten_phase2 after test_gl, then
draw_phase2 with each page's group 3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player's world ticks in the frame by default (#1428). When its ticks
take longer than 50 ms, every frame fell behind and ran the client timer's
ten, so the picture stalled at 1-2 FPS (a player's report on an AMD 680M,
in-frame tick at 36.7 ms). Vanilla's world ticks on the integrated server's
thread, which falls behind without costing frames and logs "Can't keep up!".
A frame now stops running world ticks once they took a tick's length and
drops the rest, logging vanilla's overload warning (1 s + 20 ticks behind,
at most every 10 s + 100 ticks). Ticks of a few milliseconds still get the
full ten per frame; sprints and server clients are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings main's shader api 2 (#1414) up the stack. One path for blending:
the terrain programs' blend.* now go through Pass.blend/buffer_blends like
composite and final (java_pack::pass_blends). depthtex1/depthtex2 are one
source, DepthSolid: Horizon's depth before translucent geometry, then the
pack's solid terrain's. Horizon's world goes to the pack's first terrain
target (colortex1 for MakeUp).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. The parity probe prebuild repeats each harness's own -p and features selection, so harness builds after the prebuild compile nothing (0 of 10, was 10 of 10). The full harness set ran on it locally in #1453's before/after diff.
🤖 Agent: merged after verify passed, with local parity proof (103/103 on this head). GUI sprites are measured in GUI units whatever the pack's resolution, as in vanilla: inventories, tooltips and toasts look right with Faithful 32x and 64x.
🤖 Agent: merged after verify passed, with local parity proof: parity --changed passed 103/103 on #1458's head, which contains this PR. Faithful 64x reproduced a startup panic on main (a 32768 px shared atlas over the 16384 GPU limit); with the fix it reaches the world. A pack that can't fit now falls back to the default resources, as vanilla does.
🤖 Agent: merged after verify and CI parity passed; release dry run 39935111815 was green (the smoke test drew a world, no blocker, it would publish as the release and latest). One channel, as Ludovic decided on 2026-10-23.
🤖 Agent: merged after verify and CI parity passed; lan_e2e (hands, styles, feedback, chat, frames, riding, mounts, beds, contacts, worldspawn) passes on the server thread and in-frame (bugs-st-2).
block_survival and state_provider hashed the name as an artifact instead of
passing it; decorated_region runs chunk_dump through chunk_generation's binary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed. No runtime change for players: #1445 deletes 10 tools/ scripts that nothing references (no harness depends on them); #1446 exports the mod-SDK symbols only on wasm32, so the guest crates link on macOS, and the mod sandbox tests pass.
🤖 Agent: merged after verify passed. No runtime change for players: #1445 deletes 10 tools/ scripts that nothing references (no harness depends on them); #1446 exports the mod-SDK symbols only on wasm32, so the guest crates link on macOS, and the mod sandbox tests pass.
Behavior's Timed was copied into the villager, animal, monster and warden
brains: one copy now, in brain.rs, drawing its duration in Java's int
arithmetic (two copies used plain +, the same for every duration they
have). MoveToTargetSink's tryComputePath and reachedTarget, copied into
the villager, monster and warden brains on the same WalkTarget and Slot
types, are the villager brain's, given each brain's
CANT_REACH_WALK_TARGET_SINCE. NearestLivingEntitySensor's scan, copied
into the villager, animal and monster brains, is brain.rs's living_near.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify passed; docs only (AGENTS.md, CLAUDE.md symlink, docs/architecture.md). Every path and harness name was checked against the repo.
datafix: LeavesFix's getSideMask is ChunkPalettedStorageFix's, line for
line; chunk_leaves uses chunk_flatten's. mobs.rs: push_tests takes
sensor_scan_tests' 46-line cow MobType instead of its own copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
81 client examples that harnesses run become modules of examples/probe:
harnesses run `probe <name> ...` (or `cargo run --example probe -- <name>`),
and the shared helpers (audio_common, hud_fixture, chat_screen_fixture)
move with them. The benchmarks and manual tools stay standalone examples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main.rs now only lists the probes; dispatch.rs runs them. The runner leaves
main.rs out of a probe-binary harness's dependencies and keeps dispatch.rs,
so adding a probe no longer reruns (or flake-checks) every harness of its
crate. It also follows crate::<probe> references between probes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Eight benches (block_facts, chunk, entity_geometry, json_readers, load,
save, streaming, tick_chunk_membership), lan_flat, save_parity and
vanilla_diff: no harness, tool, doc or workflow names them (review plan,
section 1, dead code). -2,222 lines; Git history keeps them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Now that the probes are modules of one binary, the helpers copied between
them live once in examples/probe/support: the collision probes' shape
printer (emit, values, box_values, bounds, 8 copies each), the canonical
NaN bits of f64 and f32 (50 copies), the registry JSON-to-NBT reader (15)
and the JSON block position (4). -1,097 lines of copies.
f64::NAN.to_bits() is 0x7ff8000000000000, the literal some copies used,
so every probe prints the same bytes.
tools/parity.py follows include!: three probes include another probe's
source, and stay dependent on it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 168 horizon-world examples the parity harnesses run become modules of
one example, examples/probe, that dispatches on its first argument;
crate::args() gives each the command line its example saw. Linking ~170
binaries, each instantiating horizon-world's generics again, was most of
the parity job's probe build (review plan, section 1).
The harnesses build `--example probe` and run `probe <name> ...`.
tools/parity.py counts only the probes a harness ran, and the support
modules they name, as its dependencies, so editing one probe still reruns
only its own harnesses (and PARITY_RUST_SCOPE keeps working).
The six manual tools (benches, loot_fill, structure_blocks, vanilla_diff,
attribute_memory, behaviour_load_profile) stay separate examples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A record naming an example since renamed or deleted made the whole Cargo
run fail ("no example target named ..."), so no probe was prebuilt and
every harness built its own: the probe binaries rename ~250 of them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With Faithful 32x the inventory showed the top-left quarter of its
background stretched over the screen, tooltips lost their background and
toasts doubled in size (#1442): the GUI code drew every atlas sprite in
texels, where vanilla draws in GUI units whatever the pack's resolution.
- Atlas sprites carry their GUI size: the size of the default resources'
texture of that name (what vanilla's `blit(..., 256, 256)` calls and
sprite sizes assume), their texels only for what copies pixels (font
sheets, fire frames). Textures only a pack has keep their own size.
The texture size filter reads the GUI size too, so Faithful 64x's
1024x1024 container backgrounds are no longer dropped.
- Nine-slice sprites follow `GuiGraphicsExtractor.blitNineSlicedSprite`:
their `gui.scaling` width, height and borders (Faithful's tooltip
background is 50x50 with a 10-pixel border), `stretch_inner`, the
single-quad and three-part cases, and borders capped at half the
drawn size by integer division.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shared GUI atlas was always 4096 texels wide, so Faithful 64x needed
4096x32768: past the 16384 texels a side GPUs allow, and wgpu panicked
in create_texture at every launch until the pack was removed (#1442).
- The atlas widens (4096, 8192, 16384) until it fits within 16384 a
side, so Faithful 64x loads. Its height rounds up to a multiple of
1024 instead of a power of two: the default resources' atlas drops
from 4096x8192 to 4096x5120, Faithful 32x's from 4096x16384 to
4096x12288.
- Textures that still don't fit are an error, not a GPU panic. At
startup it rolls the packs back as Minecraft.rollbackResourcePacks
does: the game starts with its own resources, and the selection is
cleared and saved (clearResourcePacksOnError), so the next launch
doesn't fail again. The existing "Resource reload failed" screen
shows the reason.
- The renderer refuses an atlas larger than the device allows with an
error instead of a wgpu validation panic.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged after verify and CI parity passed; release dry run 39935111815 was green (the smoke test drew a world, no blocker, it would publish as the release and latest). One channel, as Ludovic decided on 2026-10-23.
AGENTS.md (CLAUDE.md links to it): the crates and what each owns, how to
build and test fast, where each subsystem of horizon-client lives, how Game
is organised, the two single-player paths and how to test both, the vanilla
reference, and the rules that bite. docs/architecture.md: feature -> files ->
checks for the 40 areas changed most in the last 30 days.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 120-line hand-written central-directory and local-header parser goes:
the zip crate (already the client's dependency for world backups) indexes
the archive, zip64 and odd offsets included. Entries are still read raw and
inflated here, so as with Java's ZipFile (vanilla's FilePackResources)
their CRC isn't checked. New tests: folder entries, a nested pack root, a
Windows-style name, one broken entry among good ones, and files that aren't
zips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
free_port (7 copies), offline_uuid (4), varint and parse_varint (3 and 2),
dbits (6), fbits (4) and settled (2) move into lab.py; every caller uses
lab's. The other server harnesses keep importing free_port from
test_dedicated_server_parity, which now takes lab's. Imports the moved
code left unused go too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-mods' runtime is no longer a default feature, and its sandbox test
requires runtime and pack instead of a dev-dependency on itself (that
dev-dependency turned the runtime on in every workspace build with examples,
the parity probe build included). The client gets a mods feature that turns
the runtime on; without it game/mods.rs keeps content mods and logs why code
mods don't start, and every hook does nothing.
Release builds (client and server) pass the feature, CI's clippy and tests
pass it (and pack, for the sandbox test), and pr_ready does where horizon-mods
is checked. horizon_mods::find moves to manifest.rs: content mods use it
without the runtime.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The file is generated from the pinned version lock, and test_gen_version_consts
checks JAVA_VERSION against it; deleting constants by hand breaks the next
regeneration and that test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A host build of a mod (cargo build or test on the workspace) made a cdylib
whose component exports macOS's ld couldn't take: its exported-symbols list
cuts `horizon:mods/[email protected]#init` at the `#`, so coords-hud and
horizon-mods-test-guest failed to link. The exports now exist only for
wasm32; on the host the macro still checks the mod and keeps it used.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
command_parse_summary, structure_start_oracle, fetch_client_assets,
compare_structure_starts, profile_behaviour_cache, command_tree_diff,
compare_loot_fill, compare_structure_blocks, test_lan_lifecycle and
PROFILE_SHARED_JSON.md: no workflow, doc, harness, test or other tool
names them (review-plan.md, Dead code). History keeps them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
world_spawn starts a vanilla and a Horizon server for each of 60 seeds,
one at a time: 1,144-1,548 s on the CI runner, the parity job's longest
harness and its floor whenever a PR's changes reach it. With
PARITY_SHARDS = 4 each shard creates every fourth seed's worlds, in its
own build directory.
The shard helper datafix had moves to tools/parity_shard.py, which both
use. It stays out of lab.py, which every harness imports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The probe prebuild ran `cargo build --workspace --examples --bins` (or
named targets with --workspace). The harnesses build with `-p <crate>`, and
Cargo unifies features over the selected packages: under --workspace,
flate2, libc, memchr, syn, thiserror, postcard and digest get other
features for horizon-world, and horizon-mods (pack) and horizon-telemetry
(upload) for horizon-client. So every probe the prebuild made was a
variant no harness used, and each harness rebuilt its crate and its probe
again while the others waited on Cargo's lock.
Record each probe with its command's package and feature flags and
whether that command builds examples (which turns dev-dependencies on),
and prebuild one Cargo run per such selection. --bins and --examples are
recorded too, so the fast-profile horizon-client build that
save_roundtrip and world_reopen run is prebuilt as well. A harness that
never ran here takes its probes from the shared dependency lists instead
of a build of all ~275 examples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
cargo test --workspace built and linked all ~275 examples, 59% of the
test build (review-plan section 1), though none of their tests run.
Test the libraries, binaries and integration tests, then the doc tests,
as tools/gate.py and tools/pr_ready.py already do. Clippy --all-targets
still type-checks every example, and the parity job builds and runs them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
93 pub items (78 functions and methods, 14 constants, 1 struct) in horizon-client,
horizon-world, horizon-core, horizon-friends and horizon-server-files had
no caller anywhere in the repository: not in the crate, the other crates,
the examples, the tests or the binaries. rustc can't flag them because
horizon-client and the others are libraries, where a pub item counts as
used. The imports and the private helpers only they used go with them
(title.rs date_string, goals.rs flag_index).
Also drops the #[allow(dead_code)]/#[allow(unused)] items that only hid
dead code: chat_debug_fill, merchant_title and Goals::locked_by had no
caller, and four stubs (_keep, _unused, _rule_kind, const _) existed only
to keep an import used. LightTables::from_parts is used by its tests, so
it is #[cfg(test)] instead of allowed.
Protocol id tables and entity data serializer ids stay: they mirror
vanilla's registries in full. The launcher's three unused items are left
for a launcher PR.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every release is now published as the release (one channel, Ludovic's
decision of 2026-10-23), so the launcher drops its Stable/Alpha choice:
the Updates row in Settings, `horizon-launcher update-channel` and its MCP
tool, the channel on the Versions page, in `versions` and in the footer.
It updates to the newest game release that is not a pre-release (GitHub's
latest) and looks every 30 minutes. The pre-releases of the old Alpha
channel stay listed and installable by hand, never as an update. Old
`update_channel` / `beta_builds` keys in settings.json are ignored.
On Linux a release build that starts no longer rewrites the
applications-menu entry to point at itself. It writes the entry only when
there is none, or when the program it starts (TryExec, else Exec) no longer
exists; turning the Settings switch on or `desktop-entry` still points it
at this launcher, and HORIZON_LAUNCHER_DESKTOP_EXEC is still honored. A
test build started with the real HOME took over a working entry and then
left it broken when its target/ was deleted (2026-10-23).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic's decision (2026-10-23): no more Alpha and Stable. release.yml
publishes every game release as the release and GitHub's latest instead of
a pre-release that promote.yml made stable once a day. The checks that
gated a promotion run in the release itself: `decide` holds the train while
an issue labelled release-blocker is open (forced runs too), and the publish
job runs `release_train.py gate` right before publishing: the Mac mini's
smoke-test.json must be there and show a world was drawn, and no blocker may
be open. Otherwise nothing is published.
Notes cover everything since the last release that is not a pre-release, so
the first single-channel release lists all of it since v0.2.0. promote.yml,
tools/release_channels.py and its tests are deleted. The cadence is
unchanged (5 minutes of settling, 20 at most, release-now at once).
Docs and site: one channel in release-flow, versioning, security, the FAQ,
install, the home page and the Releases page (Game / Launcher filter); no
player-facing Alpha wording left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server never sent particles: /particle on single player's server thread
drew into the server's own engine, and TNT exploded without its burst for
anyone but an old-mode host. The protocol gains
ClientboundLevelParticlesPacket and ClientboundExplodePacket; /particle
sends the particles to each viewer in range (every player without
viewers, as PlayerList.getPlayers) and an explosion sends its packet to
the players within 64 blocks; their clients draw them as
handleParticleEvent and handleExplosion do.
/particle's spread was measured from the command's source as if it were a
position; it is the spread as given (Vec3Argument.getVec3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server never sent its scoreboard, so a sidebar shown on single
player's server thread or to a LAN guest stayed empty. Each tick a placed
player is now sent what changed since it was last sent the board, as
ServerScoreboard's callbacks do: every team, and the objectives in a
display slot with their slots and scores (all of it on joining).
A server's client also finds its team's sidebar by its profile name rather
than single player's "Player".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) played only three of the level events the server
sent (a block broken, the dragon egg's and a consumable's teleport): a
spawner's burst, waxing and scraping copper, a composter filling and the rest
showed nothing. Every one now goes through the handler the in-frame game uses
(LevelEventHandler).
Bone meal's level event (1505) played its sound but never its growth
particles, in either mode: it now spawns them as BoneMealItem.
addGrowthParticles does (spread over a grass block's, nylium's, netherrack's
or moss's neighbourhood and water, inside a growing block, below rooted dirt
and mangrove leaves). The server also sends it to the player who used the
bone meal, as vanilla's levelEvent names no player.
Waxing copper with honeycomb and an axe or hoe's transforms (wax off, scrape,
strip, till) make their particles and sound on the clicker's own client, as
vanilla's client runs Item.useOn; the server sends them to the others only.
The transform's preview is shared with the in-frame use.
`horizon-ctl status` counts the particles made by type. lan_e2e's new
`sparks` check: the host and the guest each use bone meal on wheat, wax
copper and take the wax off, and a spawner spawns a zombie; both sides make
the particles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A splash or lingering potion, an experience bottle or an eye of ender a
player threw was never an entity a server tracked: on single player's server
thread, for LAN guests and on a dedicated server the item was used up and
nothing flew.
The server now numbers them like its other projectiles and tracks them as
vanilla's entities: splash_potion, lingering_potion and experience_bottle
with ThrowableItemProjectile.DATA_ITEM_STACK (the potion, for its colour),
and eye_of_ender. A client draws them where the server puts them and drops
them when the server removes them.
lan_e2e's new `thrown` check: the host and the guest each throw a splash
potion and an eye of ender, and both sides see them fly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server told joining players of a default border and never of a
change, so on single player's server thread and for LAN guests the wall
neither showed nor stopped anyone. Players now get the real border on
entering a level and each change as BorderChangeListener sends it.
The player's movement collides with the border's walls
(CollisionGetter.borderCollision) instead of being clamped after moving,
which a sprint-jump got past. A guest (and single player's own player on
its server thread) outside the border takes its damage on the server, as
the host's player did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client of a server (single player on its server thread, LAN guests)
ignored ClientboundUpdateAttributesPacket for its own player, so a new
maximum health never reached it: its hearts stayed at ten and the health it
was sent was clamped to 20. It now applies each synced instance's base and
modifiers (handleUpdateAttributes) and re-applies the received health.
BossBar::new started hidden; ServerBossEvent starts visible.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
save_roundtrip found an allay saved with InLove 0: Horizon's `animal`
flag is broader than vanilla's Animal class.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A connection's chat keeps the server's component: SystemChat goes
through the same translation and styles as the action bar, and player
and disguised chat are decorated from their chat type's translation,
parameters and style (ChatType.Bound.decorate), so colours, bold,
hex colours, and the client's language are kept (#1372, #1389).
- Components parse `click_event` (suggest_command, copy_to_clipboard; never
open_file from a server) and `hover_event` show_text (#1377), and the
chat screen draws the hovered part's tooltip (#1381).
- `§` codes in chat colour the text with Chat Colors on and are stripped
with it off (ComponentRenderUtils.wrapComponents) (#1379).
- A command's operator broadcast skips the player whose command it was on
the integrated server too (#1373), and /give fires InventoryChangeTrigger
before its message, whose announcement is sent first (#1375).
- The control socket's `messages` gives the chat lines as components;
lan_e2e `styles` checks a styled tellraw, the advancement order and the
missing echo for the host and the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The remote client sends Interact for a targeted mob only for a hand
vanilla's client-side Mob.interact would accept with it (leads, name
tags, food, buckets, shears, saddled mounts, tamed pets, ...), so a bow
drawn or food eaten while aiming at a cow goes on to useItem (#1350).
- Both hands: the main hand's item, then the off hand's entity, block and
item (Minecraft.startUseItem). The server runs Interact with the packet's
hand (GuestUse::Entity), and mobs, armour stands, leads and mounts use
the held hand's stack, so an off-hand bucket milks a cow and off-hand
wheat feeds it (#1362).
- The entity tracker sends an item frame's item and rotation when they go
back to empty and 0, so a punched frame shows empty on clients (#1355).
- lan_e2e `hands`: the bow, the golden apple, the off-hand bucket and wheat
and the emptied frame, for the host and the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Guests' player data saves Fire, Air and fall_distance and puts them back
on their body when they come back (single player's players are guests
of the integrated server), so a burning player still burns after a
reopen (#1323); the old single player's level.dat keeps fall_distance
too (#1327).
- Every Animal saves and loads InLove, so a fed animal stays in love
across a reopen and is not fed again (#1396).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A section's shadow is its box swept away from the light; when the box
is outside one of the camera frustum's planes and the sweep never
crosses it, nothing it casts can be seen (Iris's advanced shadow
culling, conservatively). The picture is the same; BSL's 256-block
shadow distance draws far fewer sections.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Java pack's shadow program (shadow_cutout, shadow or shadow_solid for
the solid layer, shadow_water or shadow for the translucent one) draws
Horizon's terrain from the light after begin, as Iris's ShadowRenderer:
- shadowModelView and shadowProjection are Iris's ShadowMatrices, with
the pack's shadowMapResolution, shadowDistance, near and far planes,
shadowIntervalSize and sunPathRotation; shadow programs get them as
their model view and projection.
- The renderer picks every resident section inside the light's box (no
cave culling, no face culling) and draws it instanced, solid then
translucent, with origins after the camera's.
- shadowtex0 holds everything, shadowtex1 the depth before translucent
terrain, bound as depth textures for comparisons and as float copies
for plain reads; shadowcolor0..7 take the pack's formats and clears
(white by default).
A shadow program that fails to compile leaves the pack without its
shadow map, with a warning; its terrain programs still run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pack's terrain program draws the solid sections the last test saw,
the boxes of the others are tested against the pack's depth in a pass
of their own (OpenGL's layout: y flipped, depth 0 near, so a second
depth-only culling pipeline), and the program then draws the sections
in sight from the culling's phase 2 draws. The flat path, which has no
occlusion culling, is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When a Java pack has a terrain program (gbuffers_terrain, or the program
Iris falls back to), Horizon's world pass skips its own terrain and the
pack's program draws every visible section into its render targets,
tested against the depth of the rest of the world; gbuffers_water draws
the translucent layer before the first composite with the pack's blend
modes. depthtex0/depthtex1 get the depth before and after water.
- translate: a geometry mode (group 2, attributes as globals a glue
function fills before main, alpha test) and a pass that ends switch
cases which return, which naga's WGSL writer otherwise rejects
(Photon).
- java_terrain: the glue decoding Horizon's terrain vertices from the
arena pages into Iris's attributes, the block.properties id table by
sprite name, and a 2D atlas built from the sprite arrays (animated
sprites copied in).
- RGBA16/RG16/R16 targets are 16-bit unorm where the GPU renders them:
Chocapic13' packs its G-buffer into them.
MakeUp Ultra Fast, Chocapic13' and BSL draw their own terrain and water
on the M4 Pro; Photon still draws black (compute and .dat textures).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The comma form of modf (`v.y = trunc(x), x - trunc(x)`) printed without
brackets in a declaration's initialiser, which naga's GLSL front end then
read as a second declarator. A unary + around it has glsl-lang print the
brackets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The translator now legalises what these packs write and naga refuses:
lookups without a level outside fragment shaders become textureLod at
level 0; struct varyings travel as one varying per field; textureGather
is emulated with texelFetch; a bool given to a numeric constructor
becomes cond ? 1 : 0; `const in` parameters lose the const; array fields
of private structs get their natural stride (naga lays every struct out
as std140); modf into a vector component assigns it instead of passing a
pointer. shaders.properties keeps the empty lines the preprocessor
leaves, so Photon's `) \` before `#else` ends its value.
real_packs_load now also parses every translated program again as WGSL,
which is what wgpu is given.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:24:55 +02:00
890 changed files with 13582 additions and 25020 deletions
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
## Crates
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~190 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
@@ -45,7 +45,7 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs, shader packs and data packs:** resource packs work, Java shader packs run their full-screen effects (experimental), and data packs load functions, predicates and item modifiers.
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
@@ -57,7 +57,7 @@ Screenshots, captured in the Rust client itself with vanilla assets and no mods
</tr>
<tr>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics, Advanced Graphics and Shader Packs"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
Loaded 100 of 890 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.