ServerPlayer.postEffects lives on each guest, is saved as post_effects,
sent on join/respawn and at the end of the tick when dirty.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The chains in assets/<ns>/post_effect/*.json are read from the active
resource root at run time and run over the frame after the entity outline
and before the GUI, through Horizon's own stand-ins for the vanilla post
shaders. ClientboundPostEffectsPacket sets LocalPlayer.postEffects.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified: CI verify passes; pr_ready --quick clean but for the candle cake test that fails on main too; piston harnesses pass; the vanilla path is unchanged (nothing is refused without a mod). Follow-up to #4739's review. Queued.
The other half of main's command_exec failure (since #4702): the oracle's
Services resolve no profile, at once, so /fetchprofile fails on the next
tick. The game asked Mojang's services over the network on a thread that
the harness's back-to-back ticks outran: no line in the recorded ticks.
The probe gets a discovery address that refuses at once
(HORIZON_API_DISCOVERY=http://127.0.0.1:9/), and the harness waits for
lookups in flight before each scripted tick (profile_fetches_pending).
925/925.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
startClientLoad on login and respawn, the loading start game event, the
player's section, the 30 second timeout and the close delay. The
LevelLoadingScreen stays up (by the respawn's portal reason) until the level
is ready, and PlayerLoaded is sent then.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Port MultiLineEditBox, MultilineTextField and AbstractTextAreaWidget's frame and
scroll bar as multiline_edit::MultiLineEdit: wrapping to the box, caret and
selection moving by line, word and page keys, character and line limits, mouse
placement, drag and wheel, and the character count. TextInputHandler.addControl
sizes the box by its height or max_lines.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
IntegratedServer.reportChunkLoadFailure / reportChunkSaveFailure: region read
errors and writer-thread write errors are logged, checked for low disk space and
queued for the window, which shows SystemToast.onChunkLoadFailure / OnChunkSaveFailure
(red title) and onLowDiskSpace (ten seconds).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
#4680 registered them in the one tree every server uses, so single player's
and a LAN world's /help and Tab offered them (chat_screen and command_exec
failed on every PR's parity). Vanilla registers them only when
Commands.CommandSelection.includeDedicated: tree_extra::register_dedicated
holds them, vanilla::build_for(dev, dedicated) adds them, and the dedicated
server turns them on with set_dedicated_commands in preflight (single
player's server thread never runs it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
can_place_on and can_break (blocks, state properties, exact components),
armor trims, instruments and banner patterns with inline holders, and
provides_trim_material, decoded from their stream form.
Part of #3518
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
food, consumable, use_remainder, use_cooldown, damage_resistant, tool, weapon,
equippable, repairable, death_protection, blocks_attacks, piercing_weapon,
kinetic_weapon, swing animations, use_effects, attribute_modifiers, break_sound,
the fuels, compostable and mob_visibility, decoded from their stream form into
the codec form HashOps sees.
Part of #3518
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A client isn't asked whether a piston may move: replaying the retraction's
block event, it would pull the block the server kept. When the level
refuses a pull, the event isn't broadcast (as for a push that fails), and
clients get the piston's and the head's block changes instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Containers, pot decorations, block state, bees, typed entity data, sign text,
the NBT-only components, dye and mob variants, scalars, potion custom effects
and profiles are decoded from their stream form into the codec form HashOps
sees. The profile's skin patch is now read as vanilla writes it (texture ids,
slim flag) instead of two strings per texture.
Part of #3518
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Lead on/off, Leashable.tickLeash (elastic pull incl. quad, snap), save/load of the
holder, entity link packets, quad leash render for boats, lead drop on destroy.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The official server ran out of threads tonight (EAGAIN starting one, which
stopped its region writer until #4726). The report now says how many
threads the server runs and what the system allows: Max processes from
/proc/self/limits, and the container's pids.current/pids.max and
memory.current/memory.max from /sys/fs/cgroup. Linux only; null elsewhere.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: CI verify passes; local pr_ready (fmt, clippy, tests: the one failing test fails on main too); local parity 125/128 affected harnesses pass, command_exec and chat_screen differ only by main's new /op, /ban... (e1a530a71), remote_fireball passed on rerun; end to end with the SMP mod on a dedicated server (horizon-mods#72 proof). Queued.
getRopeHoldPosition: main-arm side, the eased yaw/pitch (Mth.lerp(pt*0.5, yRot, yRotO)) and getEyePosition(pt).
SnifferSoundInstance: follows the sniffer, stops when it is removed, has a target or stops digging.
openWorldCheckWorldStemCompatibility: reads the enabled data packs' dimensions and a world that
still needs file fixes, so the backup question comes on the first open.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The server tells the players tracking a landed falling block to show the
block for a moment, and the client draws it as a moving block until its
section mesh has been rebuilt or a second has passed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
askForBackup / openWorldCheckWorldStemCompatibility: a world with
legacy_custom_options or a non-vanilla dimension asks for a backup first.
openWorldCheckDiskSpace: under 64 MiB usable asks Continue or Back.
openWorldLoadLevelData: a world opened by name (control socket) whose
level.dat does not read shows the Recover World Data screen, and a world
opened without a title no longer becomes a new random-seed world.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
LivingEntity's arrow and stinger counts are synched entity data (12, 13):
the server counts a hurting arrow hit and a bee sting, lets them fall out
as LivingEntity.tick does, and the clients draw them with StuckInBodyLayer's
placement (ArrowLayer, BeeStingerLayer).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A player standing on a happy ghast is 4.0 above it: the carrier test
now takes from a block above to 4.5 below (carries_player, tested). A
claim at the height where the ground last held the player, within 10
ticks, isn't judged: the block may have gone server-side (broken,
pushed by a piston) before the client heard.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified. Fixes the official server's 21:54Z crash (region writer panicked on EAGAIN when spawning a helper); unit test covers refused helpers; pr_ready --quick green.
VideoSettingsScreen.addOptions/displayOptions/qualityOptions: the Exclusive
Fullscreen Mode row, the Exclusive Fullscreen and Graphics API options, and the
Texture Filtering and Anisotropic Filtering options (greyed out until the
terrain sampler reads them). Exclusive fullscreen takes effect through winit's
exclusive video modes; the Graphics API option drives Horizon's own API setting
and asks for a restart.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
LocalPlayer.startRiding starts RidingEntitySoundInstance for an AbstractNautilus (underwater loop);
RidingEntitySoundInstance reads the ridden entity's isUnderWater. ServerStatusPinger falls back to
pingLegacyServer when the status ping fails, and a protocol mismatch shows as incompatible.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The region writer shares a batch's compression out to scoped helper threads.
thread::scope's spawn panics when the system refuses a thread, and on the
official server (a container at its limit of threads, EAGAIN) that panic
stopped the writer: chunks and entities failed to save for four minutes, then
the server stopped with "the region writer stopped".
Helpers now start with Builder::spawn_scoped; one that can't start leaves its
share to the others, and the writer thread takes chunks too (helpers - 1
threads plus itself, as many as before), so at worst it does the whole batch
alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.lookAt tells the player's client; /rotate also used the
target's eyes instead of the source anchor.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: R8c approved by Ludovic, retargeted to main after #4552, main merged, full pr_ready green, CI green. The review found no defect. Its focus-state note: shell::action and button() draw no focus either, so nothing regresses. Queued.
On the official server (62 players, 1,445 mobs), tick_sculk_entities took
2.0 ms and entity_portals 1.5 ms a tick, 7% of a 47.9 ms tick.
- sculk: whether the block under a mob is climbable, or a redstone ore, comes
from per-state tables built with the others in sculk_prepare, instead of a
tag lookup by string and an Arc<str> clone per mob; a step knows whether a
warden made it from the caller, instead of a scan of every mob.
- portals: canUsePortal is asked only for the entities that have a portal
state (in a portal or cooling down), not for every mob, whose check scanned
every server player; positions before the tick go in an FxHashMap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Gui.addInitialScreens queues AccessibilityOnboardingScreen first while onboardAccessibility is set:
the title box, Narrator (inactive without a narrator), Music & Sounds, Accessibility Settings,
Language and Continue, fading in over 2 s and out over 1 s, Escape or Continue finishing it
(Options.onboardingAccessibilityFinished). GuiGraphicsExtractor.map draws only the decorations
with renderOnFrame, so the player arrows and markers no longer show on the cartography preview.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A server mod that guards land (the SMP's claims) can now refuse what changes
blocks without a player's break or place: a fluid flowing or fire spreading
into a block (native_block_spread), a piston's move (native_piston_move), an
explosion's blocks (native_explosion_detonate takes out the spared ones), a
bucket (native_fill_bucket) and a player's blow on an entity
(native_attack_entity, on the server's attack packet, so frames, armor
stands, animals and players). horizon-world asks through two LevelAccess
methods that allow by default; fire asks after its random draws, so a
refusal leaves vanilla's random order as it was.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The structure block's edit screen with vanilla's layout and lang keys, opened
by using the block (BlockEntity.usedBy), over a shared edit-screen runner the
jigsaw and test blocks will use.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The server side of the structure block: ServerGamePacketListenerImpl.
handleSetStructureBlock, StructureBlockEntity.saveStructure/placeStructure/
detectSize/loadStructureInfo, StructureTemplate.fillFromWorld + save, the
StructureTemplateManager cache and generated/<ns>/structures/<path>.nbt,
StructureBlock.neighborChanged/trigger/setPlacedBy, and placeInWorld on the
live level.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
StructureBlockEntity (fields, saveAdditional/loadAdditional, renderMode,
getRenderableBox, the corner scan) as a block entity kind, synced in the
chunk update tag like saveCustomOnly, and ServerboundSetStructureBlockPacket.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Part of #3518. UpgradeProgress counts the file fixes and the data upgrade; the
world-opening screen draws vanilla's layout from it, with Cancel.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
GameProfileArgument.SelectorResult.getNames and the five commands that use
it, in vanilla's tree shape and permission level (admins).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified. pr_ready --quick passed; 31/32 affected harnesses passed locally, and lunge_motion fails on main too, see the body. Hook points API 21 for the SMP's claims and teleports. Nothing changes without a native mod.
Ludovic decided on 2026-10-30, after the Team asked (levzzz5154: they are often
blocked on the launcher), that a trusted reviewer or Ludovic can pick any open
pull request to main for the Nightly, reviewed or not.
Kept: only Ludovic and the listed reviewers, the `paused` line, the
CODEOWNERS-guarded files, MAX_PRS, MAX_BUILDS_PER_DAY, open and targeting main,
the build without secrets, and Ludovic's `remove`. Each pull request is built at
the head it was picked at.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A chunk leaving the render distance popped to far terrain:
- Its sections were drawn only within the render distance, or in columns
far tiles still leave out, so once fading out (leave_section) they were
culled at once: the fade ran unseen. Columns with sections fading out
are drawn past it now.
- Far terrain's coarser blocks stand a little out of the real ones, and
0.2% of the distance behind did not keep them back: the tile coming in
under a column hid its sections the moment it was meshed with it, held
or fading. Sections past the render distance (lod::HELD) and fading out
go 2% of their distance toward the camera.
Measured frame by frame over a forest, render distance 10 to 3: the
change spreads over about 0.33 s instead of landing in one frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Skylight goes straight down generated columns: water and leaves take a
level a block, one span per level as a real chunk's light has them.
Full light to the bottom lit sea beds as if in the open, and coarse cells
merging water and sand showed that light as white dots across the sea.
Solid blocks pass it on: under them far terrain shows cliffs and
overhangs, which vanilla lights sideways (they went black).
- Kelp and seagrass in saved and generated chunks are the water they hold
(loaded chunks: #4144).
- Grass takes vanilla's colour at each cell (BiomeColors' grass with its
swamp and dark forest modifiers), not the biome's flat one.
- Buried gaps are filled only where no open air reaches them through
neighbouring gaps (a flood fill): the air under a forest canopy was
filled with the floor block, pillars up to the leaves.
- The seal far tiles put where near terrain begins is opaque only: near
water carries on into far water, and the seal showed through it as a
light wall.
- A section beside a chunk not loaded meshes against its own edge column,
not lit air: the outermost chunks' water drew a light sheet toward it
(fluids show both sides).
Seed 1234, LOD Distance 512: 9.27M far quads become 11.22M, the trunks and
forest floor under canopies.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The renderer cut far tiles whose nearest point was past the LOD distance,
a circle, every frame, while the plan kept tiles out to a square: tiles
popped out at the circle as the player moved, before the plan dropped
them and before their fade-out could run. The plan's range is now the
same circle (TileKey::reach) and the renderer no longer cuts by distance
(the frustum only): a tile leaving the range leaves the plan and fades out
(upload_lods), as tiles entering it fade in.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Unloading popped where loading faded:
- A section whose column far terrain now draws (a shorter render
distance, the player moving away) fades out over it, chunk by chunk as it
came, and is released once it has (Renderer::leave_section), instead of
in one frame. Columns far tiles still leave out stay drawn until they
don't (#3765), then fade out the same way.
- A far tile leaving the plan with nothing to replace it (out of range: a
shorter LOD distance, the player moving away) fades out. A tile replaced
by new tiles, or by near chunks fading in, stays whole behind them as
before: two patterns fading at once left holes between them.
The fade code gains a fading-out range (65 to 128 sixty-fourths, per
level 129 values): a section's largest code, 129, keeps its UV step exact
(checked in tiles_fade_in_on_their_own_grid).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New far tiles fade in over the ones they replace, and new near sections
over far terrain, instead of appearing at once. Each chunk-sized column of
a tile (wider ones for coarse tiles) starts in turn over the first half of
600 ms and fades over the second, as a fine ordered dither fixed to the
terrain: its unit is the power of two of a block nearest 1.5 pixels, so it
stays fine up close, does not sparkle far off and does not crawl as the
camera moves. The tiles replaced stay whole behind until it is done: no
pixel drawn twice, no hole.
- The fade rides in the instance's UV step (negated past a fade code);
terrain.wgsl and GLSL packs take the step as fract(abs(w)), unchanged
for whole tiles and sections.
- A fading tile or section is drawn a hair toward the camera; far tiles
0.2% of their distance back, so near chunks win over tiles still under
them (near water lies a little below far water: a smaller step left two
waters, opaque).
- Water and other see-through faces are not dithered, and near water is
drawn before far water, replaced tiles' last: depth keeps each layer to
where the one before has none.
- A tile re-meshed around new near chunks or finer tiles keeps drawing its
old mesh until they are in; a tile over nothing shows whole (its hidden
chunks opened holes to the sea bed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Decoration (real trees and structures) runs on a third of the threads, up
to 12, at idle priority, instead of one; batches of 8x8 chunks share
their proto border (2.25 protos a chunk instead of 4): 16.6 ms a chunk
instead of 26.9.
- Tiles up to level 2 (cells of 4 blocks) are decorated, not only 0 and 1;
a detail request asks for up to 512 chunks instead of 32, which capped
detail at one decoration thread's pace.
- Tiles mesh across the sampling threads, each with its own material
cache, instead of on the far terrain thread alone.
- Only the tiles the plan keeps are sampled, each at the level its
distance calls for, nearest first: far terrain grows outward from the
player in rings, whichever way they look. Coarse tiles sampled first
under fine ones beside the player cost a far tile's work for terrain
replaced moments later. The view-ahead focus and Lod::update's yaw go.
- The stop flags are set under the lock their threads wait on: a thread
between its check and its wait missed the wake, and closing the world
waited forever (more likely with more threads).
Fresh world, LOD Distance 512: every tile sampled after about 12 s instead
of 15; real detail after 40 s 500k cells instead of 447k.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A generation batch hands back its neighbours too, and drain_details put
every one of them, cached or not: the append-only region files grew by
the same chunks each session until the build key changed. The build key
already clears stale chunks, so one cached stays as it is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Far terrain decorates the chunks of its nearest ring with the world's own
generator (vanillagen/lod.rs), kept in memory only, 1024 of them: every
session generated them again. They now go to
dimensions/<dim>/data/horizon_far_terrain/, one append-only file per region
(lod/cache.rs, about 6 KB a chunk deflated), and a detail request reads
them back before asking the generator.
- Saved chunks still take precedence over cached ones.
- A file written by another build, block registry or format is cleared on
first use; a torn last record (a crash mid-write) is cut off.
- Only detail requests read the cache: reading it for every cell of the
coarse tiles' sampling slowed their sampling more than it saved.
Rejoining a world at LOD Distance 512: 343k refined far cells after 45 s
with the cache, 244k without.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractContainerScreen.tick closes the container of a dead player; a guest's
chest stayed counted as open (lid up) after its death.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A move that says it ends on the ground is judged when nothing there can
hold a player: only air and fluids around the feet, no collision a tenth
of a block down, no boat, minecart, shulker or happy ghast near. The
false claim is dropped, so vanilla's fall damage follows, and the levels
send back, alert and kick as configured. Creative, spectators and
gliders are skipped. Off unless [anticheat] turns it on.
horizon-bots: --height and --ground. anticheat_e2e --check ground: a
hovering bot and one on water that claim the ground are flagged and
kicked, bots walking and standing on the ground never are.
server_bench --ground: survival bots on a flat world's ground.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified. lan_e2e boat_shoved fails on main (boat stuck, 70 'moved wrongly' lines) and passes here (paddles 3.0, 1 line); mounts and riding pass; boats fails the same on main (fist breaking, unrelated). pr_ready --quick green.
A warning repeated on every packet filled the official server's log at up to
6,700 lines a second (16 MB in 30 minutes). Past 100 warnings or errors from
one line of code in a minute, the log says so once, counts the rest, and says
how many it left out when that line logs again in a later minute. Below the
limit the log is vanilla's, line for line; info lines (chat, joins) are never
limited.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PauseScreen.createPauseMenu: the Report Bugs and Give Feedback icon buttons open
CommonLinks through a trusted ConfirmLinkScreen. PlainTextButton on the title's
bottom text opens Credits & Attribution.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ServerGamePacketListenerImpl.tick sets vehicleFirstGood/LastGood to where the
vehicle is at every tick. Horizon set it only on boarding and after an
accepted move, so once the server moved the vehicle itself (/tp, a piston,
a push), the rider's moves were checked against a stale position: each one
failed, was sent back, and the client's answer failed again. The boat stayed
stuck and the official server logged up to 6,700 "moved wrongly" lines a
second.
lan_e2e boat_shoved: a piston shoves, then /tp moves, the boat the host and
the guest each ride; they paddle on, with at most a few "moved wrongly" lines.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The forgiveness dispatch's default arm is the tame gate, which needs
extra.angry_at — a field only the wolf's anger writes. The enderman and
the zombified piglin hold their persistent anger as their target (the
stare goal, the hurt-by path) and the iron golem keeps a timer and its
target, so none of the three were ever forgiven when the player they
were angry at died, while vanilla's NeutralMob.playerDied ends the anger
of all three through stopBeingAngry (Enderman and ZombifiedPiglin are
NeutralMob implementers in 26.3, checked with javap).
Their arms clear the target the dying player held, the last hurt by
with it, and for the golem the timer; anger at another mob stays, and
the tame, bee and polar-bear paths are untouched.
Follow-up to #3507, closed for main carrying the area and gate fixes:
this is the piece main still misses, on a fresh small branch as asked.
Co-Authored-By: ZCode <[email protected]>
A slot the mod's screen hides was still in the server's menu, so a shift-click
could move items into a slot the player cannot see. The server asks the same
native_screen when it opens the menu and the menu's clicks and quick moves skip
those slots. Also documents that data slots are 16-bit and start empty.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Frustum.getNearPlaneBounds, then stairs and slabs by outline, snow at 8 layers, others by isSuffocating.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
LocatorBarClient's arc slept 50 ms between positions, which drifts from the
server's ticks: on a busy runner the stab of `arc 8` came 8, 9 or 10 server
ticks after the jump, on vanilla as on Horizon, so the falling and sprinting
motions differed run to run (#4526's parity run failed on it). Each position
now waits until the game time moves past the latest `time query gametime`
reply, so the stab of `arc N` lands N ticks after the jump on both servers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keys are kept by position (SDL scancodes), as 26.3 keeps them, but named by what the US layout writes. Each key press now records what the player's layout writes on that key (winit's key_without_modifiers), and display_name shows it, as SDL_GetKeyFromScancode names keys with SDL's default keycode options: letter and symbol keys only, Latin letters only on a letter key, digits kept on the number row. Fixes#4592.
Fixes#4567: wear_banner only set the white_banner id, so SetEquipment
sent a plain banner and clients without PatrolLeader drew white cloth.
Keep Raid.getOminousBannerInstance on armor[0] and treat that stack as
captain for CustomHeadLayer.
Co-Authored-By: Cursor <[email protected]>
ServerGamePacketListenerImpl.noBlocksAround, getMaximumFlyingTicks and
tickPlayer's clientIsFloating / aboveGroundTickCount check, and
handleUseItemOn's second build-limit message for a failed placement on top
of the world.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Main moved and conflicted in `NetConfig`: its new `horizon_clients_only` field now sits beside `owner_profile` (faf7d52f2). Rechecked on main: `parity.py --changed` 110/110, `pr_ready --quick` pass, lan_e2e `tab`, `self` and `chest` pass. Queued again.
Fixes#4428: the title early-return skipped advancement, tutorial and
pack toasts, so a resource-reload failure never appeared there.
Co-Authored-By: Cursor <[email protected]>
🤖 Agent: pr_ready --quick green on af5528ef2; dedicated_server, server_entities, server_player_damage pass; anticheat_e2e: a 1.5x timer bot is flagged, set back, alerted and kicked, 6 legit bots never flagged, nothing runs when [anticheat] is absent; 60-bot A/B: off = main within noise
native_tell reaches nobody inside a player's swap (the host is out of
self.lan there): a claim's refusal is told on the action bar instead,
through the acting guest's queued packets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tick takes the block entity out of the world while the mod runs. If the
mod removed or replaced its block meanwhile, the saved items were dropped with
the taken value. They now spill like Containers.dropContents does on removal.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Gate for this part (1/3), on 785796786 merged with main, Mac mini M4 Pro (Metal). **License**, checked against CONTRIBUTING's new rule on other projects' code (#4482): - FidelityFX SDK v1.1.4 is commit c6efa6bf7f20. At that commit, `LICENSE.txt` and the header of every file this port follows are AMD's MIT text, copyright line included: all ten `sdk/include/FidelityFX/gpu/opticalflow/*.h`, `sdk/src/components/opticalflow/ffx_opticalflow.cpp` and `gpu/spd/ffx_spd.h`. - `optical_flow.rs` follows `ffx_opticalflow.cpp` (its passes, their order and their dispatch sizes). So I added the same notice at its top and named that file in `THIRD_PARTY_NOTICES.md`, with a link to the SDK at the commit (785796786). Nothing else changed. #3763 and #3764 contain the old head and need this merged in. **Proof:** - `pr_ready --quick` passes. `parity.py --changed` has nothing to run: the selector maps these files only to lan_integration, which runs when named or nightly. - `shader_validates` and `finds_a_shift_and_a_scene_change` pass on Metal. **Cost per frame:** - Off: zero. `OpticalFlow` is constructed nowhere until part 3. - On: measured locally with a throwaway test (not pushed) that dispatches it on two alternating frames, 100 frames after a warm-up: | size | GPU, median (p90) | CPU encode (debug build) | |---|---|---| | 1280×720 | 2.33 ms (2.96) | 0.27 ms | | 1920×1080 | 3.30 ms (3.76) | 0.29 ms | | 2560×1440 | 4.68 ms (5.53) | 0.31 ms | | 3840×2160 | 8.48 ms (9.35) | 0.32 ms | Part 3 should keep this in mind: on an M4 Pro the flow alone costs a fifth of a 60 fps frame at 1080p, before the interpolation, so frame generation only gains where the real frame takes much longer than that. Keep it off by default, as planned. - Smaller notes, none blocking: - `dispatch` creates the `prepare_luma` bind group every frame; it could be cached per colour view. - The scene-change "last workgroup" handoff relies on relaxed WGSL atomics being visible across workgroups, which holds on current drivers. Queued for the merger (`skip-release`: players see no change).
A submission with repo (and no file) opens a pull request adding an entry that
links the public GitHub repository; its versions are the repository's releases.
An update through the form of a project that has a repo is refused with
'publish a GitHub release in <repo>'. Projects without a repo keep the file path.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch (97f2c57e5, 6350e5219). On main, an equipment kind now holds every layer (leather's overlay is layer 1). So the namespace resolution now runs for each layer's texture, and `content_mods_equipment_loads_under_its_namespace` expects a list of layers. `hud.rs` keeps your namespaced sprite names with main's decoded image. Proof, on main with #3741 and #3742: - `parity.py --changed` 110/110; - `pr_ready --quick` pass, including both new tests. - In the game: pr-triage-3's pack test at your heads (comment on #3740) showed amethyst, copper and chorundum armour drawing their own textures. - A root without content mods: `content_namespaces::namespaces` is empty, so no extra folder is walked, and every vanilla texture still takes the `strip_prefix(textures)` branch. Same sprites as before. Queued after #3742.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch through #3741's carry (no conflicts of its own). It's proven with #3741 and #3743 on main: `parity.py --changed` 110/110 and `pr_ready --quick` pass. Queued after #3741.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch (fd41f0e36; `mod_port_probe.rs` now uses main's `PackOverlay` loaders). I also made the one change pr-triage-3 asked for in its pack test on #3740 (a502eccc9): the attribute normaliser drops 1.21.1's `player.` prefix too, so spears' `entity_interaction_range` matches the oracle. Proven together with #3742 and #3743 on main: `parity.py --changed` 110/110 and `pr_ready --quick` pass. Queued for the merger; #3742 and #3743 follow.
native_right_click_block hears a player's right click on a block before
the block's own use and the item's, and may cancel it; native_acting_player
says who acts inside a player's hook; native_run_command runs a command as
the server's console (teleports across levels); server players carry
their health. What a server mod needs for land claims and teleports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hub::sound used the server tick as ClientboundSoundPacket.seed, so two
sounds in the same tick picked the same client variant. Level.playSound
draws soundSeedGenerator.nextLong per call.
Closes#4576.
Co-Authored-By: OhMyAgent (grok-4.6) <[email protected]>
An entry names its GitHub repository (repo). The page fields may come from
horizon-registry.toml in that repository (the entry's own win), and the versions
the registry's CI read from the repository's releases are kept in
versions/<id>.json and listed beside the entry's own [[versions]], which become
optional. cargo horizon-mod registry release|page run a submission's checks on a
release and print the version/page for that cache. The index schema is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: docs only (docs/smp.md), checked at 4cbfa78: the gambling rules match the orchestrator's decision of 2026-10-30 (on for the official server only, off in the mod by default; in-game currency only, never bought with real money or cashed out, no paid random items), and the coinflip lines match horizon-mods #58 as merged; pr_ready and CI green.
optical_flow.rs follows ffx_opticalflow.cpp (passes, order, dispatch
sizes), so it keeps the SDK's copyright line and MIT notice like the WGSL
port; THIRD_PARTY_NOTICES names that file and links the SDK at the v1.1.4
commit, as CONTRIBUTING's rule on other projects' code asks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The registry's index will list the release assets of a mod's own repository
(https://github.com/<owner>/<repo>/releases/download/<tag>/<name>), so an author
ships a version without a registry pull request. The launcher accepted only the
registry's own releases; it now also accepts that exact address shape. The
index's SHA-256 still pins the bytes.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A new crate, horizon-anticheat, holds server.toml's [anticheat] settings, each player's violation
levels (they decay every second) and the checks' rules, with no game state. The dedicated server's
hub runs the checks where it handles packets and carries out a flag: send the player back, alert the
console, the log file and online operators, then kick; never a ban. Each check has its own toggle and
thresholds. Without [anticheat] (the default, and the generated server.toml only has a commented
example) the hub holds no anticheat and the server does what vanilla's does.
The first check is timer: vanilla's client ends each tick with ServerboundClientTickEndPacket at the
server's tick rate, so a client further ahead of the wall clock than 300 ms is flagged, with a capped
balance for lag spikes and slow server ticks.
horizon-bots gets --timer F (a sped-up client), tools/anticheat_e2e.py runs a real dedicated server
with legit and cheating bots, and server_bench.py --anticheat measures the cost.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An owner's new version with nothing else of the entry changed gets an HMAC
trailer (REGISTRY_UPDATE_KEY) in the pull request body and the label
owner-update. Off when the secret is unset.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
LocalPlayer.startRiding starts a RidingEntitySoundInstance for a happy
ghast (entity.happy_ghast.riding, volume 5 * clampedLerp(speed, 0, 1)),
as it does the two inside sounds for a minecart; Horizon only had the
minecart's. Port the instance: it stops when the player gets off or the
ghast is silenced, and is silent under water and below 0.01 speed.
Part of #3518.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
Resolves a repo, release, tag, ref or commit link through GitHub's public API to an
exact release .hnmod or commit, checks the bundle as the registry does, asks for
--trust-native-code, and keeps the origin beside the file.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
1.21.2 renamed player.entity_interaction_range, player.block_interaction_range
and the other player.* attributes along with generic.*, so a spear's
interaction range never matched the oracle's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on the Mac mini at 7a4f7d5: tools/server_horizon_only_e2e.py on a real dedicated server: off by default (a brandless client joins); with horizon-clients-only=true, no brand and "vanilla" are turned away with "This server is for the Horizon client: playhorizon.dev" and a console line, "horizon" joins. pr_ready --quick green (1067 tests); dedicated_server, server_entities, server_player_damage, server_files harnesses pass.
🤖 Agent: requeued: main merged in (main had dropped the two native_lifecycle source-shape tests, so main's file is taken), docs/api/server-reference.md regenerated for the new command; horizon-server tests 53/53, report and dedicated tests pass; end to end verified earlier (init/start/report 10/stop).
The crossbow model picks its charged form from the stack's
`charged_projectiles` component. Nothing set it for a mob that loads its own
crossbow, and the mob renderer and the equipment packet kept only the item
name, so a mob's components never reached the model at all. Set and clear the
component with the pillager's charge state, draw the stack the mob actually
holds, and keep the stack the equipment packet sends.
Fixes#4521.
Co-Authored-By: Hermes Agent (deepseek-v4.1-flash) <[email protected]>
ItemEntity.tick zeroed Y on any vertical collision, so items never
got slime restitution. A moving piston carrying slime has no
bounceRestitution of its own. Landing now bounces at 0.8 (non-living
Entity.restituteMovementAfterCollisions), and a piston slime launch
clears on_ground so ItemEntity.tick does not skip movement.
Vanilla: ItemEntity.tick, Entity.move, PistonMovingBlockEntity.causeBounce.
Co-Authored-By: OhMyAgent (grok-4.6) <[email protected]>
TheEndGatewayBlockEntity.saveAdditional writes Age and exit_portal, then
puts ExactTeleport (as true) only when exactTeleport is set. getUpdateTag
is saveCustomOnly, so vanilla's chunk packets and BlockEntityData never
carry ExactTeleport = false. entity_tag wrote the key unconditionally;
it now only writes it when the flag is set, as save.rs already does.
The test checks both cases round-trip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In-game currency only, never bought with real money, never cashed out, no
paid random items; off in the mod by default, on for the official server.
How a coinflip's stakes are held, as the mod does it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BeaconBlockEntity, TheEndGatewayBlockEntity and CreakingHeartBlockEntity
override getUpdatePacket (ClientboundBlockEntityDataPacket.create) with
getUpdateTag = saveCustomOnly, so vanilla puts their data in the chunk
packet (BlockEntityInfo.create) and in the BlockEntityData that
ChunkHolder.broadcastBlockEntityIfNeeded sends for a block the level
reports changed (sendBlockUpdated, /data merge block). The LAN host's
entity_tag had no tag for them, so guests got none: a gateway's Age (its
spawning beam) and a beacon's effects never reached them.
entity_tag now gives the three their saveAdditional fields: primary and
secondary effect and Levels; Age, exit_portal and ExactTeleport; the
creaking UUID. The rest of the row from #3520 was already in place: the
host's block log is its sendBlockUpdated, and sign edits, sign dyes, ink
and wax, shelves, campfires, vaults, spawners, trial spawners, decorated
pots and /data already push to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on the Mac mini at d39eb5d: tools/server_reach_e2e.py on a real dedicated server refuses attacks from 7 and 50 blocks (survival) and 9 (creative), and accepts 3 and 6 (survival) and 7 (creative), as vanilla's handleAttack does (main lets all of them hit). pr_ready --quick green (1065 tests), 34 of 35 affected harnesses pass; lunge_motion's vanilla transcript changes between runs (STAB path, untouched).
Every button that signs in is now Microsoft's "Sign in with Microsoft",
drawn to Microsoft's published branding (41 pixels, the four-square
symbol from its official SVG, dark #2F2F2F / light white with a #8C8C8C
border, semibold 15 pixels): the Overview's account row, preparing
without the game, the sign-in sheet and Settings. The New instance
dialog's loader cards show Fabric's and Quilt's logos.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerScoreboard.addPlayerToTeam sends the old team's leave
(createPlayerPacket REMOVE) before the new team's join, and a client's
Scoreboard.removePlayerFromTeam throws IllegalStateException when the
player already sits in another team. The LAN host's scoreboard diff sent
teams in name order with joins before leaves, so moving a player to a
team sorting earlier (red to blue) made a vanilla guest drop the packet
with an error and Horizon's remote client take the player out of the
new team. All leaves now go before any add or join.
The remote client's handleSetPlayerTeamPacket leave now only takes a
player out of the named team, as removePlayerFromTeam(name, team) does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An instance's icon, in the sidebar, Home's hero and its dialog, is now its
own picture first: one the player chose (Choose image…, scaled to 128
pixels and kept as icon.png in its game folder) or its modpack's icon,
written when the Mods page installs a pack. Else the block or colour
chosen in the dialog; else its mark: the loader's logo (Quilt's and
NeoForge's shipped under CC0 and CC BY 4.0, Fabric's and Forge's fetched
from their own sites), the grass block for plain Minecraft, Horizon's for
Horizon. The letter tile is the last resort.
The dialog's icon tiles sit in rows of eleven: a wrapping row was
measured as one line and the plate ended above the buttons.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: docs only (docs/smp.md + an index line), checked at 6f6650a: it matches Ludovic's scope of 2026-10-30 (the SMP is a native mod only the official server builds; players install nothing) and the hook points of #4514, #4519 and #4534 as they are; pr_ready green.
🤖 Agent: verified at e23b4b7d8 (stacked on #4519): pr_ready green (1072 horizon-client tests, clippy); dedicated_server and server_entities pass; the SMP mod's /trade on a real dedicated server with two real clients clicking the server's chest menu: closing, the cancel button, death (keep_inventory on) and leaving moved nothing; a full inventory was refused with nothing moved; a confirmed trade swapped 10 diamonds for 5 emeralds and 35 coins, as counted on both clients.
🤖 Agent: verified at 396f45655 (stacked on #4514): pr_ready green (1069 horizon-client tests, clippy); dedicated_server and server_entities pass on #4534's head, which holds this one; with the SMP mod's market, a real dedicated server and two real clients ran /sell wheat, /sell hand and /buy torch 5, and the buyer's own client counted 0 diamond, 0 wheat and 5 torch; a creative player was refused.
🤖 Agent: verified at 768d20d: pr_ready green (1067 horizon-client tests, clippy -D warnings); tools/parity.py dedicated_server server_entities pass (on #4534's head, which holds this one); with no mod the tree stays Null and the gate is a hash lookup; with the SMP mod, a real dedicated server and two real clients ran /balance, /pay, /baltop and /eco (operator-only, hidden from others), the files and audit log matched, and a restart kept the balances.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Proven on ccb85be48 merged with current main, on macOS: - `pr_ready --quick` passes (launcher rustfmt, clippy, tests). - Concurrent removals through the launcher CLI in a scratch home: three versions of 20,000 files each removed at once, with `versions --installed` polled 143 times meanwhile. None of the three is listed once its removal has started. On main, each stays listed for ~0.46 s of a 1.5 s delete, half-deleted, which is the window your marker-first change closes. - The window's gate (`removable`) is covered by the new test. Queued for the merger.
Through mods/port-item-stats with main merged. Conflicts: main's
equipment layers became every layer of a kind (leather's overlay), so each
layer's texture now resolves its namespace there; hud.rs keeps the
namespaced sprite names with main's decoded image.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: 3 interleaved A/B rounds on playhorizon (60 bots) cut tick_block_entities ~5.7 to ~2.7 ms/tick, MSPT avg -3.5 to -9 ms; behaviour unchanged (the snapshot is only skipped on ticks that read no mob); pr_ready --quick green.
each_world_preset_writes_its_own_generator joined the preset id
(`minecraft:normal`) to its temporary folder, and Windows refuses `:` in
a file name (os error 267), so the test failed there on every run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Off by default. With horizon-clients-only=true in server.properties (server.toml's [properties]), a
client whose minecraft:brand isn't "horizon" (vanilla's client says "vanilla"; one that names none
counts as vanilla, as on vanilla's server) is disconnected during configuration with "This server is
for the Horizon client: playhorizon.dev", and the console logs which client it was. The brand can be
spoofed: this is a gate for players who didn't install Horizon, not security.
horizon-bots gets --brand and prints a configuration disconnect's reason;
tools/server_horizon_only_e2e.py checks both settings on a real dedicated server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: main's integrated_open (owner_profile joins its NetConfig), the
level shapes beside owner_profile in NetConfig, SyncData::load's pack
overlay, and main's lazy skin drawing in update_remote_skins (the offline
skin is stored when its slot is gone, as the offline cape is). Also brings
back conn.rs's prompts_become_the_components_vanilla_sends test, which the
branch had replaced by mistake.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Replace hand-rolled base64 decoding and SHA-1 hashing in crates/horizon-client-render/src/skins.rs with the base64 and sha1 crates already used across the workspace, preserving the existing decoding fallbacks and compatibility tests.
Fixes#4431
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: new crash test passes, pr_ready --quick green. A dedicated server's crash report says Server thread and drops the client's render settings.
`horizon-server report [SECONDS]` (or `horizon-report` on the console)
records every tick for 60 s by default, up to 600, then writes
reports/perf-<time>.json and a short .txt beside it, and prints the
summary. The report holds:
- TPS and MSPT (average, p50/p95/p99, max, ticks over 50 and 100 ms) and
CPU time per tick;
- each span's share of the tick, and the 10 worst ticks with their spans;
- entities by type and chunks per level, players online;
- memory, CPU, the machine, view and simulation distance, max players.
No names, UUIDs, addresses, seed or world name, so a server owner can
attach it to an issue as it is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#4052 changed VanillaCreativeTabsExporter, which the preparer runs, so the
prepared output changed (digest 5e50c240...) while OUTPUT_VERSION stayed 2.
Every parity run on the Mac mini's runner since fails "the prepared output
changed", and players would keep resources prepared before it. The digest is
the one the runner's own preparation of main gave, twice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HoneyBlock.entityInside already slides players and mobs. Dropped items
never ran it, so they fell at full speed beside honey. ItemEntity.tick
now applies isSlidingDown / doSlideMovement after the move, before
friction, without slide sounds (doesEntityDoHoneyBlockSlideEffects).
Fixes#4513
A client could attack any player or mob from any distance: the server path (LAN host, the integrated
server thread, dedicated servers) never checked ServerboundAttackPacket's range, so a hacked client
hit people 50 blocks away. Now, as ServerGamePacketListenerImpl does:
- handleAttack: Player.isWithinAttackRange(mainHandItem, targetBox, 3.0), the held item's
attack_range or AttackRange.defaultFor (0 to the entity interaction range), for players, mobs,
vehicles, dragon parts and projectiles alike; out of range the attack is dropped silently;
- handleInteract: Player.isWithinEntityInteractionRange(targetBox, 3.0);
- digging and handleUseItemOn read the guest's block_interaction_range attribute instead of a fixed
4.5 (5 in creative), so /attribute and items count, and handleUseItemOn logs vanilla's
"Rejecting UseItemOnPacket ... too far away from hit block" line.
The ranges are the player's attributes: creative's modifiers (+2, +0.5) are on them every tick.
horizon-bots gets a stand behaviour, --attack NAME and the victim's health in its report;
tools/server_reach_e2e.py proves it on a real dedicated server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified: unit test fails on main (601 descriptors) and passes here; bots repro shows region fds growing without bound on main; pr_ready --quick green. Fixes the official server's 'Too many open files' crashes.
Every mod row shows the mod's own icon: Modrinth's for a mod the browser
installed (one bulk request per instance, the id-to-URL map kept in
mod-browser/project-icons.json), else the icon the jar names in its
fabric.mod.json, quilt.mod.json or mods.toml, else a glyph. Icons are
decoded off the UI thread and scaled to the row's size.
The image cache moves under the launcher's data folder, keeps each
image's ETag/Last-Modified and revalidates week-old images with a
conditional request. The Mods tab builds only its visible rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a dedicated server, the main thread's report said "Thread: Render thread"
and listed the render settings it had read from options.txt (the official
server's EMFILE crash reports did). The main thread is now named for its
side, as vanilla's reports name it, and a server's report leaves out the
render settings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CopperGolemAi's core activity has InteractWithDoor at priority 3: it opens
the doors on the path and closes the ones passed. The brain had none, so a
golem whose way between two chests ran through a closed door never got
through. The mob_ai probe now plays the sound of a door a mob opens or
closes, with the pitch drawn from the level random.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A server mod opens a vanilla chest menu (generic_9x1..9x6) for a player and
fills it with items to look at. No click moves anything: clicks and the
menu's closing (by the player, another menu, leaving) reach the mod through
native_menu_clicked and native_menu_closed, run with no player swapped in.
native_exchange_items swaps two players' offered stacks in one step, each
checked to still be exactly as offered and to fit whole on the other side,
or moves nothing. A stock client shows it all.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The region cache in save.rs opened every region, entities and poi file a
dimension ever touched and never closed one. A server whose players travel
(teleports, elytra, exploration) runs out of file descriptors: the four
crashes of the official server on 2026-10-30 (v0.2.97) were all
"Too many open files (os error 24)" from horizon-region-writer, then a
failed save and a fatal tick error.
Cap the cache at RegionFileStorage.MAX_CACHE_SIZE (256): when full, drop
the least recently used file no caller still holds. Also raise the soft
open-file limit to the hard one at startup, as the JVM does with
-XX:+MaxFDLimit, so sockets and other files keep their headroom.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The SMP uses only what a stock Horizon client renders (commands, vanilla
chest menus, sounds, titles, action bar, boss bars, sidebar). The trade
menu is display only and swaps both offers atomically, so no way out has
items to return. The config is config.json, as the mod writes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
IronGolem.canSpawnSprintParticle: a golem that moves draws nextInt(5) each
tick and, one time in five, the particle's two coordinates, even though a
server drops the particle. Horizon drew none, so a golem's random drifted
from vanilla's. iron_golem_village_0/1 and iron_golem_outside_village
(GolemRandomStrollInVillageGoal, MoveBackToVillageGoal) now match vanilla.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_held, native_count_items, native_carried_items, native_take_items,
native_room_for and native_give_items: what a server shop needs, on the
36 inventory slots and the off hand, plain stacks only (no damage, no
component), in Inventory.add's slot order, and give only where it fits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Launcher redesign R7b (the instance's Mods tab), approved by Ludovic after the screenshots. main merged in; full pr_ready green, verify and launcher CI green on this head. The only comment is the overlap bot's note on #4478 (different change; not landed).
The guest sprints at the host, releases Ctrl (the sprint holds while W
stays down), and left-clicks the host. Vanilla's attacker side of
causeExtraKnockback cuts its horizontal speed to 0.6 and ends the sprint:
the speed drops below the walking speed and climbs back to it. On main
the guest keeps its sprint speed (0.1532) after the hit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_register_commands: the commands a native mod adds to every player's
command tree (usages such as `pay <player:player> <amount:integer>`,
optional arguments last, an operator level each), merged into one tree the
hub sends after vanilla's, filtered by level. native_perform_command: a
player's command on the server for a registered name, inside their swap,
whether the world allows commands or not. Helpers: the players on the
server, a line to one or to all, the command's feedback, the world's
folder. With no mod hooking them, the tree is empty and nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The row said FollowFlockLeaderGoal was missing: it is ported (#3026) but had
no parity scenario. cod_school, salmon_school and tropical_fish_school
(five fish each, 400 ticks) found two differences: a leader recruits the
fish in vanilla's getEntitiesOfClass order (sections by x, z, y, then
entry order: Mob.section), which decides who is left out of a school of
at most five; and the oracle and probe draw the goal's first start delay
again once the mob's random is seeded. All three scenarios now match.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Horizon's own server stays plain: no SMP command, setting or code in the
release. The mod lives in horizon-mods (native/smp); Horizon gains general
server hook points (commands, players, items, menus, interactions) that
any native mod can use. Config and data move to world/horizon-smp/.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Built in behind [smp] in server.toml, off by default: why not a server mod
or a datapack, the commands, the configuration, the data files under
world/horizon-smp/, the cost and the phases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on main + this head. With the level-shape stack merged, this PR is now one file (biome.rs, +242/−68): the biome-table piece levzzz5154 asked for first on #3492. I merged main in, and main's biome features (#4211) are now read in BiomeInfo::from_json, where this PR parses a biome file. Checks: 110 of the 111 harnesses this change can affect pass. lunge_motion fails on main itself since #4297. dedicated_server, datapack_worldgen, server_world_gen, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) also pass. Biome files are parsed only when a world loads. Queued to merge.
Dolphin.registerGoals adds FollowPlayerRiddenEntityGoal for AbstractBoat and
AbstractNautilus at priority 8. The player the mob sees tells the vehicle it
steers (PlayerView.vehicle). mob_ai scenarios dolphin_boat_* (the oracle
gets a boat the script moves) match vanilla tick for tick; Mob.moveRelative
adds its zero vector like vanilla (-0.0 becomes 0.0).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
#3539: on v0.2.97 the WebAssembly Zoom's key set to R showed another key. Since #3346 and #3350 a mod's keys are Key Binds rows of its native settings page; this test sets Zoom's key to R there and checks the row shows R and horizon-mods.txt keeps key.keyboard.r. Fixes#3539.
Home gets a Mods tab after Overview (plan R0, screens 5 and 6): every mod of the selected
instance, the browser's and the jars put in its folder by hand, with its version, where it comes
from (Modrinth, needed by another mod, or a local file), an amber Update pill when the browser
found a newer version, and a switch (a Java jar turned off is renamed .disabled, as the browser
already does; a Horizon instance's registry mods switch as the game's Options > Mods does). A
filter, Update all and Add mods (the Mods page) complete it. Java instances without a loader
have no Mods tab.
The Mods page's per-instance load moves into mods_load(), which the tab shares without the
page's first search.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Launcher redesign R7a (the instance's tabs under Home's hero), approved by Ludovic after the screenshots. main merged in; full pr_ready green, verify and launcher CI green on this head.
with_heart collected a snapshot of every mob for every heart, every tick,
while CreakingHeartBlockEntity.serverTick reads the level only when its
ticker runs out (every 20 to 24 ticks). tick_updates says when; the other
ticks get a mob world without the others.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_destroy_block hears a player break a block once the game allowed it (ServerPlayerGameMode.destroyBlock) and may cancel it, which keeps the block and its drops; native_place_block hears a block placed from an item (BlockItem.place). A mod stops a placement by claiming native_use_item_on, which runs before it. Fixes#4474.
main's biome features (BiomeGenerationSettings.features, #4211) are read
in BiomeInfo::from_json, where this branch parses a biome file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on main + this head. It carries #3488 and #3489, and merging it lands both. I merged main in. main's new native_world_read.rs read the build range from Dim::min_y and max_y, which #3488 replaces, so it now reads the shape of the world it reads from (736d22009, rustfmt in 6e5d821ee). Checks on main + this branch: the 122 harnesses these PRs can affect, plus dedicated_server, datapack_worldgen (with the tall fixture), server_world_gen, server_dimension_players, locator_bar, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. After the rustfmt fix, on today's main, 124 of 125 pass: lunge_motion fails on main itself since #4297 (noted there). Meshing time is unchanged. Queued to merge; the data-pack biomes (#3491–#3494) follow.
native_use_item_on hears a right click with an item on a block once the block let it through (ItemStack.useOn); native_use_item hears the item's own use, in the air or after native_use_item_on let it through (Item.use). Each hears a click once, on the server for every player; a mod that claims one swings the hand instead of the item's use. Fixes#4290.
Items and damage parity: arrow and potion effects, mob damage sources, adventure can_break and can_place_on, lily pads on water, mud from water bottles
🤖 Agent: Nightly notes now open with the existing launcher banner (release_train.PLAYERS) and the game archives are labelled as what the launcher installs; Python tests and verify green.
🤖 Agent: verified on the Mac mini: a game started without prepared files (the release zip run by hand) now shows a 'Start Horizon from the launcher' window with Download (playhorizon.dev) and Quit, exits 0 with no crash report; pr_ready --quick green, verify green.
A player ran horizon.exe from the release's game zip, thinking it was the
launcher. v0.2.97 then failed with "resource loader stopped before
finishing" and a crash report: the loader's real error (no prepared files)
was taken by App::about_to_wait, and the next poll saw the closed channel
(fixed on main by 9f28fe890, which also checks horizon-resources.json
before any window). That check still ended in an error and a crash report.
- An interactive game with no prepared files now opens a window in Horizon's
design language (bundled fonts, none of Minecraft's files needed): start
Horizon from the launcher, Download (opens playhorizon.dev), Quit. The
console says the same. Closing it quits with code 0 and no crash report.
Offscreen, scripted and server runs keep the error.
- The heartbeat and servers.dat wait until the files are there.
- A loader channel that closes without a result always names why.
- PLAY.txt sends players to playhorizon.dev for the launcher.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Home's lower part was one long page: the setup rows, the console, worlds, servers, packs and
screenshots one after the other. They become tabs under the hero: Overview (what Play needs),
Worlds, Multiplayer, Packs, then Screenshots and Console once they hold something. Each tab names
how many it holds; its own buttons sit at the strip's end. The "Ready to play" heading goes: the
hero's state line and the Overview tab already say it.
HORIZON_LAUNCHER_PAGE=worlds (multiplayer, packs, screenshots, console) opens Home on that tab,
for screenshots; it replaces HORIZON_LAUNCHER_CONSOLE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player ran horizon.exe from the release's game zip, thinking it was the
launcher. Game releases already open with the "Want to play? Download the
Horizon launcher" banner (release_train.PLAYERS, #1647); the Nightly, the
only release since 10-28, did not. Its notes now start with the same banner
(raw HTML, which launchers' notes views skip), and its game archives are
labelled "Game files the launcher installs".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: server light-edge fix (#4294): verify, launcher and parity green on the head merged into main; playhorizon dedicated_server 5/5 pass (main 5/5 fail), no idle/join regression.
LivingEntity.addAdditionalSaveData writes AttributeMap.pack as the
player's `attributes` list and readAdditionalSaveData applies it before
the health. Horizon kept the list it loaded and never wrote the live map,
so /attribute base changes (scale, max_health, ...) were lost on reopen.
The host writes the live attributes in sync_level and applies the saved
list when its first body loads; a LAN guest does the same through
playerdata::store and its first body after joining.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The early skips in spawn_loaded_entities and load_mob matched any remembered UUID, so stale id to UUID entries dropped chunk minecarts after a reopen. Track the UUIDs restore_saved_vehicle brought in for this load instead.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Merging #4222/#4082 and #4259 left two `mod tests` in game/creatures.rs, the
pearl test calling Items::load without its PackOverlay argument, and
end_gateway's ProjectileWorld test impl with the old in_water(Vec3) signature.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's read_block took the build range from Dim::min_y and max_y, which
this branch replaces with each level's shape: it now reads the bounds
of the world it reads from (the level's own, data packs included).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged by Ludovic's AI agent (Claude). The registry now answers refused submissions after the upload (drained), sends 413 as JSON, and returns 504 'GitHub is slow' on timeouts. Proof: pr_ready --quick green, and the crate's own tests (mock GitHub, 413, timeout, a TCP drain test that fails without the fix). Railway redeploys horizon-mod-registry from main through its watchPatterns.
🤖 Agent: telemetry server only (its own workspace): cargo fmt, clippy -D warnings clean and cargo test all pass there; pr_ready --quick passes for the rest (Python tests, game workspace). Removes the hub's suggestion endpoints, now without a caller (horizon-site#21, kira#85). skip-release.
`ServerTickStart` now fires at the top of `tick_server`, before the first level's streaming
and world update, where NeoForge fires `ServerTickEvent.Pre`. Before, it fired inside the first
level's `tick`.
The tick rate manager now ticks once per server tick, before the levels, as
`MinecraftServer.tickServer` does; each level reuses its answer, like `ServerLevel.tick` reading
`runsNormally`. Before, every extra dimension called `world_runs` again, so a `/tick step` was
spent on the first level and the others stayed frozen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Launcher redesign R6 (New instance with Horizon, Vanilla and Modded), approved by Ludovic. Full pr_ready green, verify and launcher CI green on this head. Contains #4382, #4391 and #4392; lands after them.
🤖 Agent: Launcher redesign R5 (sign-in sheet), approved by Ludovic. Full pr_ready green, verify and launcher CI green on this head. Contains #4382 and #4391; lands after them.
🤖 Agent: Launcher redesign R4b (Play's steps while it gets ready), approved by Ludovic. Full pr_ready green, verify and launcher CI green on this head. Contains #4382; lands after it.
🤖 Agent: Launcher redesign R4 (Home's hero), approved by Ludovic after the screenshots. Full pr_ready green, verify and launcher CI green on this head. First of the stack #4382 → #4391 → #4392 → #4399.
The website's suggest form is gone (horizon-site#21) and Kira no longer relays it
(kira#85): roadmap suggestions come only from #horizon-ideas posts. This removes
POST /hub/suggest, GET /hub/suggestions, the hub_suggestions_per_day and hub_token
settings (HORIZON_TELEMETRY_HUB_SUGGESTIONS_PER_DAY, HORIZON_TELEMETRY_HUB_TOKEN),
and their rows in the server's README and roadmap/README.md. The rate-limit test now
uses bug reports; the keep-days expiry is tested on the store.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server tick tested blocks with an AABB sweep of the trident's 0.5 x 0.5 box
and a per-axis moved != velocity test, so a trident passing up to 0.25 blocks to
the side of a block stuck in mid-air beside it. Use AbstractArrow.tick's
zero-width level().clip(...) and AbstractArrow.onHitBlock's 0.05 pull-back
along Math.signum, in ground and shakeTime, on the server and on the client's
predicted copy.
Fixes#4365
Co-Authored-By: DeepSeek 4.1 Flash (DeepSeek Harness) <[email protected]>
A submission refused before its body is read (signed out, CSRF, the hourly limit) or partway through (a file over
64 MiB) answered and closed the connection while the browser was still sending the file. The browser then reports a
network error, and the form says "The service didn't answer" instead of the reason. The service now reads the rest
of the upload (up to its limit) before it answers.
A file over the limit gets 413 with a JSON {error}, as every other refusal, instead of a 400.
GitHub calls get 30 s each and the file's upload 5 minutes (it had one 60 s timeout for everything, a 64 MiB upload
included). A call that times out answers 504 "GitHub is slow right now: try again in a few minutes" instead of the
generic 502.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged by Ludovic's AI agent (Claude) after review against #3545 and a proof on macOS: full pr_ready on main + the whole Launch API stack (parity 111/112, 0 failed), at this head pr_ready --quick, parity on the changed files, lan_e2e chest and eat on both paths, and a real end to end (prepare into a scratch --output, then horizon-ctl launch with --game-dir/--home/--quickPlaySingleplayer in isolated homes). Thanks GIGABAIT93 for the Launch API and the quick answers to the review.
Mods pinning different Horizon commits used to be refused. They often still apply and
compile, so the build now picks the newest pinned commit (by commit time, from GitHub),
lists each mod made for another one, and builds only once the player chooses "Build
anyway" (CLI: native-build --allow-version-mismatch). Those mods are repinned on the
build's own copy, like hook-point mods. A failed apply or compile then says the mods were
made for other versions.
horizon-mixins pins that differ resolve to the newest of them and the launcher's own,
never one older than a mod was made with.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Not while the version needs preparing, prepares, waits for an account that
owns the game, or runs (review: it showed beside Needs preparing and over
the steps at 34%).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The New instance dialog gets the night look (an opaque plate cut in steps,
night buttons) and tabs for the kind. Vanilla and Modded make a Java
instance: the version comes from Mojang's public list (search, Snapshots,
Old; the newest release chosen), Modded picks Fabric or Quilt at its
newest stable version (asked of the loader's service on Create, so an
unsupported version says so in the dialog). The name follows the choice
("Fabric 1.21.4") until the player types one. Horizon instances are made as
before. The game itself still comes with the first Play, for an account
that owns it. HORIZON_LAUNCHER_PAGE=new-instance opens it for screenshots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging after CI. Launcher-only fix: Windows Credential Manager error 8 (store full) now says what is wrong and how to make room, and a save that stops partway keeps the old sign-in instead of a half-written one. Unit-tested with a fake store that fills up; pr_ready --quick green.
Windows players saw "Platform secure storage failure: Windows error code 8" when signing in
(#4313, #4366). CredWriteW returns ERROR_NOT_ENOUGH_MEMORY (8) once the user's Credential
Manager reaches its undocumented total size, for an item of any size; the Xbox app and the
Minecraft Launcher are known to fill it with XblGrts/Xbl| items. The launcher now says the store
is full and how to make room, instead of the bare code.
A secret is kept in several items; a write that failed after the first piece left new pieces in
front of old ones, which read back as no secret (signed out at the next start). The old secret
now goes back when a write stops partway.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The owner gate's screen becomes a sheet: an opaque plate cut in steps over
the dimmed launcher. "Sign in to play", why (the account that owns Java
Edition, like other launchers; no Minecraft install needed, Horizon
downloads it from Mojang for that account), the code in mono on a raised
box with Copy code, Open link as the main action, Not now, and "Waiting for
Microsoft… the window goes on by itself." The link reads as typed
(microsoft.com/link). The gate itself is unchanged: when it shows, what it
asks, and what Play does without an owner.
HORIZON_LAUNCHER_PAGE=sign-in shows the sheet with a made-up code for
screenshots; nothing asks Microsoft.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rapid clicks, as the Mouse Tweaks wheel sends, could leave a stack in the
cursor, and in creative mode add real items. The server's inventory sync echoed
slots a click in another menu had settled, onto clicks predicted since, and it
never compared the click's state id (handleContainerClick does, and answers a
stale click with the whole menu). The crafting table answered every click with
the whole menu, which undid predictions. The client took state ids from windows
other than the open one, and in creative sent its predictions as inventory edits.
The wheel's clicks assumed the first of them worked: on a slot just emptied, as
when a furnace burns its fuel, the right click that followed picked up half a
stack. It now places with a one-slot drag, which does nothing with an empty hand.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The gate only allowed the inventory and chest-like menus, so nothing moved
in furnaces, crafting tables, hoppers and dispensers. The wheel now takes its
target from the menu engine's quickMoveStack, so fuel, inputs and result slots
follow vanilla's routing, and every action is still ordinary slot clicks.
Fixes#4312
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While a version prepares, Play reads "Getting ready…" with the download's
percentage, Cancel beside it while a download from Mojang runs. Under them:
the bar, what is happening ("Downloading Minecraft 26.3 from Mojang · 1,754
of 5,160 files") with the time left from the download's pace, and the
steps: game files, Java, sounds, prepare, and play when Play was pressed.
backend::PrepareStep is set by the preparation as it moves on. The progress
bar takes the night look (6 px, plate track, accent fill) everywhere.
HORIZON_LAUNCHER_PAGE=getting-ready shows the state for screenshots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verify and both parity jobs are green at 3a11501e2 (the rerun passed dedicated_server and world_spawn#3); approved by rdbtCVS. Queued for the merger.
- `--name` leaves OPTIONS: the game still takes it, but the contract doesn't
promise offline play. The guide's first step is now the owner rule, and a
session the game can't read is stated as a fact, not an option.
- A bad value names its option (`Horizon: --width: invalid digit ...`), since
a launcher shows that line to the player.
- Session errors say "the session", true for --session-stdin too.
- --quickPlaySingleplayer takes one folder name of saves/, as vanilla's level
id; a path is refused. --world still takes a path for harnesses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's level-shape work replaced vanillagen's fixed MIN_Y and SECTIONS
with ColumnLayout, so the branch merged with main no longer built.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: R3 night frame (sidebar, top bar, account chip). Ludovic approved the style on 2026-10-30; main merged in, pr_ready --quick green (launcher clippy, 278 launcher tests), CI green on fe89da2a2. Merging.
tests/launch_api.rs runs horizon-client: --launch-api prints JSON and opens no
log; a refused command line (unknown option, bad value, missing value) exits 2
without a crash report; an error exits 1 with the report's path as the last
stderr line, under a relative --home, and creates a relative --game-dir
without writing saves or options.txt where the game was started. Paths have
spaces, as launchers' instance folders do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The selected instance's name in the display cut, a caption naming what it
plays and whether it is ready, when it was played, and the update line.
Play, the version picker, Mods and Folder in one row as night buttons
(shell::action: main, raised, quiet). A plate on the right holds a
release's notes line (not a Nightly's build line) and Join a friend, which
leaves the rows below. "Host a server" goes: Servers is in the top bar.
The picker names a Nightly as "Nightly · ba6344a" instead of its tag.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SulfurCube.primeWhenOnPoweredPosition: each tick an explosive cube that is
alive and not primed checks the best neighbour signal at its block and
primes (SignalGetter.getBestOwnOrNeighbourSignal; no block has an own signal).
lan_e2e check sulfur_cube_power.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
In vanilla Minecraft 26.3, the Quit Shortcuts option (options.quitShortcuts,
default true) controls whether keyboard shortcuts (Alt+F4 on Windows/Linux,
Cmd+Q on macOS) can close the game window. When disabled, close requests
initiated via quit shortcuts are ignored to prevent accidental quits, while
window close buttons continue to work.
Horizon already loaded and serialized quitShortcuts in options.txt and
had the cycle button wired in options_ui, but the setting was excluded from
Opt::available and the close handler never checked InputQuirks.isQuitShortcutDown.
- Add is_quit_shortcut_down in horizon-client-input::input checking
platform modifiers and physical keys (Command+Q on macOS, Alt+F4 elsewhere).
- Check is_quit_shortcut_down on WindowEvent::CloseRequested in game.rs
and startup_menu.rs when quit_shortcuts is disabled.
- Enable Opt::QuitShortcuts in Opt::available so the toggle is clickable in
Controls with its vanilla tooltip.
- Add unit tests in horizon-client-input, horizon-client-render, and horizon-client.
Fixes#4336.
Part of #3518.
Co-Authored-By: Antigravity <[email protected]>
A server pack's animated item texture (lobby:item/..., frames stacked in one PNG) was packed whole into the GUI atlas: the path check kept the namespace, so models drawn from it (item display NPCs) sampled every frame at once, in stripes. It now cuts the mcmeta's first frame and animates like vanilla's own.
Each join parsed entity-models.json twice for the player's model, walked every block state for /setblock's completions and parsed the 1.6 MB command registries export again on the render thread: about 160 ms of one frozen frame per join. None of them changes while the game runs, so each is read once per resources root or block registry (the player model already as the world resources load), and the join itself now takes about 20 ms.
register_piglin ran before the spawn weapon was equipped, so the crossbow goal was never added and the melee goal took over. Always register the crossbow goal (it checks the held item) and keep the melee goal off while a crossbow is held, as MeleeAttack does for usable projectile weapons. Fixes#4356.
🤖 Agent: verified on main + this head. I merged main in. goals.rs keeps main's restricted check and max_h, now with the world's bounds. nether.rs keeps main's ghast fly-to target. main's new occupied_pois_in_range reads the world's bounds too. One harness failed: use_item_on. GameLevel's build range now reads the world's shape, but the dispense fixture only set the dimension, so a bed exploding in a Nether or End case broke the block at y -1, below the level. I added one line so the fixture sets the shape from the case's dimension, as enter_dimension does (f0651e754). Checks: 107 harnesses, dedicated_server, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) passed on the head before that fix. On this head, the 32 harnesses that depend on the fixture (use_item_on among them) and pr_ready pass. Queued to merge; #3488 follows.
`ItemContainerContents` travels as the `minecraft:container` component, and the
client already decodes it into the tooltip's contents lines. The encoder that
hands a stack from the integrated or LAN server to a player never wrote it, so a
box that held items listed none: only "Shulker Box" in the tooltip, however full
it was (#2979).
Write the component as `ItemContainerContents.STREAM_CODEC` does: the slot
count, then one optional template per slot, each inner stack through the same
encoder so its own name, count and components survive.
Co-Authored-By: Hermes Agent <[email protected]>
SetInstrumentFunction drew the random option and dropped it, so a goat horn
from a loot table (pillager outpost chests) had no instrument.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
OminousBottleAmplifier.addToTooltip, SuspiciousStewEffects.addToTooltip and
CrossbowItem.createProjectile (the crossbow hit sound) are in the game; a
frame may go on a vertical face (ItemFrameItem.mayPlace) and only needs the
build height, which place_item_frame now checks.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
GameLevel's build range now reads the world's shape, so a case in the
Nether or the End must set it as enter_dimension does: a bed exploding
there broke the block at y -1, below the level (use_item_on).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractCookingRecipe.display() gives smelting, blasting, smoking and
campfire one and the same FurnaceRecipeDisplay: the ingredient, any fuel,
the result, the station, the cooking time and the experience. Only the
station and the book category differ, and the category comes from the
recipe, not from the client.
book_entries encoded shaped and shapeless crafting only and returned an
empty list for everything else, so the furnace, blast furnace and smoker
books were empty for every player whose window is a client: single player
on its server thread, LAN guests and dedicated players alike.
One shared arm now writes that display, and the category switches follow
the jar: smelting food/blocks/other, blasting blocks and one misc tab,
smoking, and campfire (which no book tab lists, as in vanilla).
The template slot cycles the armour trim and netherite upgrade template
icons; with a template in the table the base slot cycles what it takes (the
11 tools and armour of the netherite upgrade, the four armour pieces of a
trim) and the additions slot the ingot or the seven trim materials, one icon
every 30 ticks with a 4 tick fade. It showed one fixed netherite icon.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The server thread plays every guest's MotionEvent::HurtSound into its
own silent Audio, so single player and multiplayer heard nothing (#4149);
the client's ClientboundDamageEventPacket handler never played it either,
unlike vanilla's LivingEntity.handleDamageEvent.
Vanilla splits the sound in two: the hurt player's own client plays it
from handleDamageEvent (Player.getHurtSound: the damage type's effects,
volume 1.0, a pitch that walks 1.0 ± 0.2 on the level random), while the
server's Entity.playSound reaches every other player around but that one.
The DamageEvent handler now resolves the packet's damage type through the
synchronized registry and plays hurt_sound(effects) at the player, and
tick_guests sends the server's sound to the hub for everyone but the hurt
player (Some(conn), the same split the death sound and the honey slide
already use).
Fixes#4149
Co-Authored-By: Claude <[email protected]>
🤖 Agent: verified on main + this head. I merged main in (through the carried #3485). The crate split had moved chunk.rs, lod.rs and minimap.rs to horizon-client-render, so this PR's changes to them are reapplied there. Checks: 121 of the 122 harnesses this change can affect pass. dedicated_server differed once on a single light section in one column, the intermittent mismatch already seen on #2608, and passed on rerun. pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. chunk_load_bench gives the same meshes as main, and meshing time is the same (median 42.4 vs 42.5 µs per section over 6 pairs). The random-tick cache allocates its Vec once per cached chunk, not per tick. Queued to merge; #3487 follows.
goals.rs keeps main's restricted check and max_h, with the world's
bounds; nether.rs keeps main's ghast fly-to target (no heightmap mirror).
main's occupied_pois_in_range reads the world's bounds too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's AvatarRenderer builds the arm pose per hand and gives a brush
that is in use the BRUSH pose, whose HumanoidModel case drops the arm
twice as far as an ordinary item: xRot * 0.5 - pi/5 rather than - pi/10,
with yRot zeroed. Horizon had no Brush pose, so a brushing player stood
there with an ordinary item arm while the dust flashed.
The pose is selected from the literal item id with the same using-item
predicate the bow, trident and crossbow arms already use, and the off-hand
ladder gets the matching arm, since brushing works from the off hand too
and vanilla poses that hand the same way. Brush is one-handed and does not
affect the off hand, so shields, bows and tridents keep their paths.
The first-person sweep is not here: PR #3951 already implements it.
The no-damage guard and the knockback check read the held item, effects and
enchantments. On a client none of them count: items and effects only change
attributes on a server, ATTACK_DAMAGE and ATTACK_KNOCKBACK aren't synced, only
ServerPlayer overrides getEnchantedDamage, and getKnockback only adds
enchantments on a ServerLevel. So an empty-hand sprint hit under Weakness does
slow a vanilla client, and a Knockback sword alone doesn't.
test_remote_attack_parity runs the real Player.attack on a LocalPlayer shell
against a RemotePlayer and a zombie and compares the attack strength, velocity
and sprint flag: 51,840 cases identical on 26.3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FireworkRocketEntity.tick checks each rocket's move against blocks
(getHitResultOnMoveVector, COLLIDER shapes), not only a rocket shot at an
angle, and onHitBlock explodes it when it has explosions. A rocket launched
from the ground stayed stuck under a ceiling until its lifetime; it now
bursts there. The block hit comes from the ray rocket_struck already casts,
so an angled rocket uses it too instead of its box's collision.
lan_e2e's rocket-ceilings check covers it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server sent a column as soon as it was lit, and its light layers
counted only the neighbours already loaded. Vanilla's ChunkStatus.LIGHT
needs every neighbour at INITIALIZE_LIGHT, and its light storage counts
their sections. So a column loaded with its saved light next to a proto
chunk still being made went out without the top light section that
proto's blocks add. dedicated_server failed 5 of 5 on playhorizon since
an idle dedicated server keeps no spawn area (#4095).
A column now waits until each neighbour is loaded or a decorated proto
chunk, and a proto neighbour's non-air sections (its TerrainChunk
counts, read when sent) go into the light layers. Nothing is kept per
column.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The barrier section leaves a barrier at the test position, so the following 'Emptying water into air makes a source' step ran against a non-air block and failed for a non-creative player. Restore air there; bucket_empty is unchanged.
Co-Authored-By: jaszkus's AI agent <[email protected]>
Vanilla anchors a name tag at the entity's NAME_TAG attachment,
(0, bbHeight, 0) of the box of the current pose, and the submitter lifts it
0.5 more. The sneak flag only decides isDiscrete (the see-through pass).
Horizon took the height from the flag for players, so a player swimming or
gliding with shift held (pose box 0.6, shared crouch bit set) got a tag at
2.0 instead of 1.1, and riding with the flag gave 2.0 against vanilla's
2.3. The height now comes from m.height(), which reads the pose box.
That branch was only needed because the local player's model never filled
extra.avatar_pose (guests and server players get it from DATA_POSE), so
tick_player_model now stores the pose it already computes. Crouching and
standing heights are unchanged at 2.0 and 2.3.
🤖 Agent: verified on main + this head. I merged main in (through the carried #3484); save.rs keeps main's mob imports. Checks: the 52 harnesses this change can affect, dedicated_server, server_entities, server_dimension_players, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels: chunk packets to a LAN guest in each level) pass. The changes are per chunk, not per block. Queued to merge; #3486 follows.
FireworkRocketEntity.onHitBlock explodes only a rocket with explosions
(hasExplosion). A plain rocket out of a crossbow or a dispenser went off
with a poof at the first block it touched; it now flies on to its lifetime,
as in vanilla. Rockets with explosions still burst on impact. lan_e2e's
rocket-walls check covers both.
Fixes#4281
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla draws eye of ender with ThrownItemRenderer(context, 1.0F, true),
which only scales and billboards; the visible lift comes from the item
model. item/ender_eye parents item/generated with no display override, so
its ground transform's translation [0, 2, 0] applies: 2/16 of a block,
outside the 0.5 scale, along the camera's up axis.
Horizon's hand-rolled billboard centred on the entity, so the eye sat 0.125
blocks low. The quad maths moves into eye_quad with a named GROUND_LIFT and
tests that pin both the offset and its axis (a tilted camera must move the
quad along its own up, not world Y).
🤖 Agent: verified on main + this head. I merged main in (through the carried #3483). save.rs keeps main's mob imports. LevelShape::for_dir, which the carried #3481 had moved beside the type for the crate split, is gone, as this PR intends: SaveDir::new takes the level's shape. Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. Queued to merge; #3485 follows.
handlePlayerPositionChange calls jumpFromGround when a player on the
ground moves up without being on the ground. Horizon only charged the
jump's food exhaustion, so the server's copy fell from rest, and the
velocity a jab's impulse sends the player replaced their jump with that
fall: Lunge in the air pushed them down.
jump_from_ground already charges the exhaustion and counts the jump
statistic, which a server's players never got.
Fixes#4278
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_screen_click turns a press on a mod screen into a menu button id,
sent as ContainerButtonClick; native_menu_button runs it on the server for
the mod block entity behind the menu. native_screen_tooltip shows lines
under the mouse. NativeScreen gets its width, the inventory's left column
and own_labels, so a mod can lay out a window that isn't vanilla's. Helpers:
native_item (an item's icon, any size), native_cursor, native_screen_title,
native_block_property, native_insert_item (into a container, as a hopper).
API 9.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drop CustomHeadLayer changes now merged in main (#3605), keeping
multi-layer equipment parsing and rendering for leather overlay.
Co-Authored-By: Antigravity <[email protected]>
Vanilla's thrown snowball, egg and ender pearl are drawn by
ThrownItemRenderer, which only scales and billboards the item: the visible
lift comes from item/generated's ground transform, translation [0, 2, 0]
of 16 units, i.e. 0.125 blocks scaled by the renderer's scale (1.0 for the
throwables, 0.75 and 3.0 for the fireballs). Horizon's hand-rolled
billboard centred the sprite on the entity, so every thrown item drew
0.125 blocks low.
The quad now takes that lift, gated to the kinds vanilla draws with
ThrownItemRenderer: snowball, egg, ender pearl and both fireballs. Llama
spit shares this quad here, but vanilla draws it from
LlamaSpitRenderer with its own translate(0, 0.15, 0) along world up, so it
is deliberately not lifted.
Spawn position, eye height, rotation and velocity already matched vanilla
(the spawn differs by 1.5e-9 blocks), so this is a rendering offset only.
The test pins exact vertex bounds: 99.875..100.375 at scale 1.0,
99.625..101.125 at 3.0, llama spit at no lift, and the rise following the
billboard's own up axis rather than world up.
Not covered here: eye of ender has the same 0.125 offset at its own call
site (game/end.rs eye_geometry) and is left as a follow-up. XP bottles
already take the item-model path, which applies the same ground transform,
so the report's XP-bottle half is not explained by this change.
SheepRenderer gives a sheep named exactly "jeb_" the lerped dye colour
(ColorLerper.Type.SHEEP: 16 colours, 25 ticks each, white as 0xFFE6E6E6)
and keeps its undercoat. Horizon read no name at all, so the egg did
nothing.
The name test matches MobRenderer.checkMagicName (exact, custom name), the
lerp is Mth.lerpInt's floor per channel, and the age threaded in is the
render state's ageInTicks (tick count plus the partial tick), so the hue
moves per frame instead of stepping at 20 Hz.
The colour vectors came out of the 26.3 jar and were re-derived
independently before the tests were written; jeb_wool_lerps_through_the_
sixteen_dye_colours pins 0xE6E6E6, 0xD0A37D, 0xBA6015, 0x953A8D, 0xDDDDDD
and the 400-tick wrap, and the layer test covers the dyed, baby and
sheared cases.
Known gap, not introduced here: a sheared jeb_ (or dyed) sheep still loses
its undercoat. Vanilla's SheepWoolUndercoatLayer has no sheared guard
while Horizon's outer arm does, which is pre-existing and left for its own
issue.
🤖 Agent: verified on main + this head. I merged main in (through the carried #3482). The crate split had moved gfx.rs, gfx/resident.rs, mesher.rs, streaming.rs and world.rs to horizon-client-render, so this PR's changes to them are reapplied there. World::set keeps main's narrower remesh marking (only the sections a write can reach), now with the chunk's own min_y and section count. Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. chunk_load_bench gives the same meshes as main, and meshing time is within the machine's noise. Queued to merge; #3484 follows.
MouseHandler.onScroll gives the wheel to the open screen and turns the
hotbar only when there is none. Books, lecterns, signs, server dialogs
and the link and pack prompts are drawn over gameplay while the screen
stays Playing, so the wheel still changed the selected slot under them.
Fixes#4253
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A thrown ender pearl flew straight through an End gateway: the gateway
passes in game/end.rs only collected dropped items and mobs. Pearls now
join them (EndGatewayBlock.entityInside, Entity.handlePortal): the pearl
lands at the exit with no motion and rotation 0
(EndGatewayBlock.getPortalDestination for a ThrownEnderpearl), waits in
the arrivals queue while the exit loads, keeps its landing chunk loaded
for TicketType.PORTAL's 300 ticks (placePortalTicket), and then takes
its owner where it lands. ThrownEnderpearl.onInsideBlock fires the
owner's enter_block trigger (Remote Getaway), for a guest as them.
Part of #3514.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Advancement icons at the top and bottom of the tree lost one of their two
triangles when the scissor cut through them. HudBatch::clip_since now clips
non-rectangular geometry as polygons, interpolating UV, glint UV and depth.
Refs #4248
LivingEntity.resolvePlayerResponsibleForDamage: a hit by a tame wolf sets
the victim's last-hurt-by-player to the wolf's owner for 100 ticks, so the
kill drops experience and player-only loot, and the owner, when in the
world, gets ServerPlayer.awardKillScore (mob_kills, totalKillCount, team
kills, player_killed_entity). The killed statistic stays the causing
entity's (Player.killedEntity), so a mob's kill no longer adds it, and
Raider.die's hero and Villager.tellWitnessesThatIWasMurdered, which also
follow the causing entity, skip a kill whose blow came from a mob.
Applied after a mob's melee hit and after /damage with a causing entity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The large and small fireball hits on a mob didn't record the player who
owned the fireball (LivingEntity.hurtServer's setLastHurtByPlayer), as an
arrow's hit does. On the default single player the host plays as a guest
on its server thread, so a ghast killed by its own fireball, turned back,
was credited to nobody: no Return to Sender, Monster Hunter or kill
statistic.
tools/test_return_to_sender_parity.py strikes a still fireball into a
ghast with no AI on both single-player paths and expects the advancement.
Fixes#4212
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemStack.getDisplayName appends getHoverName, and getItemName prefers the
`minecraft:item_name` component over the item's own name (Item.getName), so
/give and the other item commands name a renamed stack after that component.
stack_display_name only read `custom_name`, so a datapack- or server-renamed
item was reported under its plain item name ("[Diamond]" for a diamond whose
item_name is "ItemName"). Read the component first, as vanilla does.
Co-Authored-By: Hermes Agent (deepseek-v4.1-flash) <[email protected]>
TamableAnimal.die sends the owner, when showDeathMessages is on and the
owner is online in any dimension, the pet's CombatTracker.getDeathMessage
("Rex was slain by Zombie"). Horizon's mobs had no combat tracker and
mob_died had no such step, so the owner was never told.
Mobs now keep a CombatTracker (mobs::combat): Mob::damage records each hit
that reaches the health (not one wolf armor takes), LivingEntity.tick's
recheckStatus forgets them after 100 ticks (300 in a fight), and /kill
records its generic_kill hit (LivingEntity.kill). mob_died tells a tame
wolf, cat, parrot or nautilus's owner, the host or a guest by UUID, a
message built by the player's own getDeathMessage code from those hits,
with the kill credit (getKillCredit) and the name of a creeper that blew
up. /damage by a player makes them the mob's lastHurtByPlayer, as
LivingEntity.hurtServer does.
Fixes#3514
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: parity rerun green (the earlier dedicated_server light-layers failure was a load flake); approved by rdbtCVS; no blocking comments. Queued for the merger.
ContainerSetData for another window (an old or replaced one) no longer
reaches the open mod screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FallingBlockRenderer draws the block through
ModelBlockRenderer.tesselateBlock, which moves it by
BlockState.getOffset(BlockPos.containing(x, maxY, z)). Horizon drew
falling blocks centred, so a pointed dripstone (OffsetType.XZ) jumped
sideways when it broke off and fell.
Fixes#4102
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_with_mod_block_entity runs the mod's change first, then marks the
chunk changed and, when items moved, lets comparators read the container
again (native_mod_block_entity_at notified before). native_set_block_property
goes through Level.setBlock with UPDATE_ALL, so neighbours and observers hear
of it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
World generation loads the loot data with no packs while the world is
prepared; opening the world then forgot it and loaded a second copy, which
the generator's copy kept alive beside it (8 MB for the whole run). Now
opening a world keeps the loaded copy when its root and packs are the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's sound sliders (Options.createSoundSliderOptionInstance) call
SoundPreviewHandler.preview when there is no level: each category plays
its own UI sound at the slider's volume (zombie ambient for Hostile
Mobs, cow ambient for Friendly Mobs, thunder for Weather...), a preview
still playing is not restarted, and moving another category's slider
stops the previous one. Horizon had no preview at all.
Port SoundPreviewHandler (sound_preview.rs), report a changed category
volume from the options screen (Event::Volume, only for a new value as
OptionInstance.set), and play the preview from the game when the
options were opened from the title screen.
Part of #3518.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A menu reads, writes and sends only the declared slot count; items saved
past it (a container that shrank) stay saved and untouched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundSetCameraPacket (/spectate, a spectator's click) was decoded as
an unknown packet, so the view stayed on the player while the server kept
that player on the camera entity: the view did not turn with the mob, sat
at the player's eye height inside the mob's body, and the client's own
moves made the server rubber-band it ("moved too quickly").
Follow ClientPacketListener.handleSetCamera / Minecraft.setCameraEntity:
the camera entity's id is kept until the player's own id, a login or a
respawn puts the view back. While another entity is the camera the view
is aligned with it (Camera.alignWithEntity: its head's yaw as
LivingEntity.getViewYRot, its eye height eased by Camera.tick), the entity is not drawn in first person, the pick starts
from its eyes, and the player neither moves nor sends moves
(LocalPlayer.applyInput / sendPosition check isControlledCamera); the
input packet still goes out, so sneaking lets go. A spectator's click
now sends ServerboundSpectatorActionPacket (Minecraft.startAttack,
MultiPlayerGameMode.spectate / spectatorNoAction).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Items become slots only when they are an item list saved exactly so; any
other Items stays in the mod's compound. A block without a container gets
its Items back in its compound before its tick, and breaking it drops
nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on main + this head. I merged main in (through the carried #3481); it merged cleanly. Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (light, levels, distance, portals, chest, forced-levels) pass. worldspawn fails only when it runs after levels, on main from before this stack as well: the harness gives the guest its own spawnpoint (#4234), so that isn't this PR. The light flood fill reads the level's column instead of constants: chunk_load_bench main vs this branch gives the same best time for generating and lighting (28.95 ms per column) and the same meshes. Queued to merge; #3483 follows.
Agents start each comment with "🤖 Agent:" (CLAUDE.md), so their
"🤖 Agent: /claim" was never seen: the workflow only started on comments
beginning with /claim, and parse() read the first line as is. rdbt
noticed agents' claims missing (2026-10-30).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaEntityDeathLootOracle sets the carried block, and the corpus kills
endermen carrying every enderman_holdable block, by a player with
Looting and by generic damage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Enderman.dropCustomDeathLoot rolls the carried block state's own drops
with a diamond axe enchanted by the enderman_loot_drop provider (Silk
Touch I) and spawns them after Monster's equipment drops. Horizon's
death drops never read the carried state, so the block was lost.
mobs::drops::die now yields a CarriedBlock drop in that place, and the
game resolves it through enderman::carried_block_drops (the block's
loot table with the silk touch axe, the enderman as this entity, from
the table's random sequence).
Part of #3513 (the Enderman.dropCustomDeathLoot row).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaMobAiOracle ran out of its 4g heap (OutOfMemoryError: Java heap
space, 34 times in one run) within about 3 minutes of a fresh run on main,
with nothing else running. Runs that reused cached oracle results kept
passing, which hid it until the scenarios changed. With 8g the fresh run
passes in 2.9 minutes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mark block entity NBT changes dirty and queue an update even when the block state is unchanged, so spawner mob previews refresh after data commands. Add an isolated screenshot regression for zombie/pig/zombie transitions.
Co-Authored-By: OpenAI Codex <[email protected]>
The explosion entity pass only reached players, mobs, items, primed TNT,
minecarts and item frames, so a blast left paintings, cushions and armour
stands standing. Vanilla's ServerExplosion.hurtEntities hurts them too:
BlockAttachedEntity.hurtServer kills a painting or cushion and drops its
item, and ArmorStand.hurtServer with an IS_EXPLOSION source runs
brokenByAnything (its equipment pops, the stand item does not) and kill.
An invisible or marker stand is spared (ArmorStand.ignoreExplosion and
hurtServer), and without mobGriefing a block-keeping or mob-caused blast
spares them all (shouldAffectBlocklikeEntities, the Mob source check);
item frames now share that gate, which also counts a projectile's owner.
Vanilla lists the entities in reach before hurting any, so the items these
entities drop are not caught by the same blast. Frames and the new
decorations now break after the item pass, so their drops survive instead
of being destroyed at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a connection (a server, a LAN world, and single player, whose world
runs on its own server thread) F3+I copied the client's partial copy of
the block or mob and called it server-side: a chest lost its items, a mob
its tags and brain. Vanilla's KeyboardHandler.copyRecreateCommand asks the
server through DebugQueryHandler (ServerboundBlockEntityTagQueryPacket,
ServerboundEntityTagQueryPacket) and copies the ClientboundTagQueryPacket
answer to the newest query.
Add the three packets, the query state on the connection and its answer,
and the server side for guests (ServerGamePacketListenerImpl
.handleBlockEntityTagQuery and handleEntityTagQuery, for game masters
only). Part of #3520 (its handleBlockEntityTagQuery and
handleEntityTagQuery rows).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundCustomChatCompletionsPacket was only an id constant, so the
names and tags proxies and plugins add with it (players on other backend
servers, chat tags) never showed when Tab completes chat. Decode it, keep
the connection's set like ClientSuggestionProvider.modifyCustomCompletions
(ADD, REMOVE, SET; an unknown action id is ADD), clear it on a
reconfiguration, and complete chat from getCustomTabSuggestions: the
players and the custom completions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
analyse_shape and the cached support flags computed every shape's six faces
through face_shape, which caches them on the shape for good: about 39 MB that
nothing reads again. They now use face_shape_transient; runtime queries still
cache.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ArmadilloAi.initIdleActivity runs AnimalMakeLove(1.0), then a RunOne of
FollowTemptation(1.25, armadillo_food, close enough at 1 block for a
baby and 2 for an adult) and BabyFollowAdult(5..16, 1.25). The
armadillo's goals had none of them, so spider eyes gave hearts but no
baby, armadillos ignored a player holding them, and babies wandered off.
Add Breed, Tempt and FollowParent, each wrapped so that it only runs in
the IDLE activity: not while ArmadilloAi.updateActivity picks PANIC
(danger in mind and not panicking) and not while it is a ball.
Part of #3512
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ModelEngine mounts a model's item displays (its bones) and interaction
hitboxes (its click boxes) on a pivot, usually an area effect cloud, and
moves only the pivot. #3776 seats text displays on mobs and the local
player through the passenger attachment; an item display riding a pivot
display or an area effect cloud, and a hitbox riding a pivot, had no
seat at all and stayed where the server first spawned them.
Every display and interaction box now asks its vehicle where to sit:
mobs through their passenger attachment, boats and minecarts from their
seat, armor stands from their box, another display or interaction from
its position, and any other tracked entity (the area effect cloud) from
its feet. Riders of riders settle over three passes, as mobs already did.
Co-Authored-By: Qoder <[email protected]>
AbstractCubeMob.push(entity) calls dealDamage on an IronGolem whose
pushEntities takes the cube, but Horizon only ported the playerTouch path,
so cubes chased golems (they target them) without ever hurting them.
The cube's ai_step now hits every alive iron golem whose box overlaps its
own (EntitySelector.pushableBy, doTeamsAllowDamage, hasLineOfSight) for
getAttackDamage with a mobAttack source, unless the cube cannot deal damage
(a tiny slime, or a NoAI cube, which is not isEffectiveAi). A new
CubeHurtMob event plays entity.slime.attack only when the hit landed, as
dealDamage does after hurtServer; its two pitch draws from the cube's
random happen even for a silent cube.
Part of #3513.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleMoveEntity and handleEntityPositionSync leave
an entity the client is authoritative for where it is; only
handleTeleportEntity puts it somewhere else, at once. The steps the
server's moves queued while the rider drove the horse replayed after the
dismount: the horse jumped back about five blocks on a vanilla server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Servers (Denizen, Nexo, ModelEngine) spawn minecraft:interaction entities:
invisible boxes with a width, height and response flag that a script
answers. The client knew nothing of them, so clicks aimed at the box hit
the block behind or nothing, and the map's NPC models could not be
clicked.
A spawned interaction becomes a RemoteInteraction (SetEntityData ids 8,
9 and 10), pickable for use and attack from its feet up its height, and
its teleports replace its position at once (Entity's no-op
InterpolationHandler). The click ends at the box: SUCCESS when it
responds, and the arm swings; CONSUME otherwise. Either way the Interact
packet carries the hand, nothing behind the box is used, and pick-empty
leaves no entity to pick.
Co-Authored-By: Qoder <[email protected]>
Projectile.onHit calls onRedirectProjectile on a redirectable target:
AIM_DEFLECT along the thrower's look, the thrower its owner, acceleration
power 0.1. #3599 made the pearl teleport but left the charge as it was.
A wind charge now runs applyInertia (power 0, inertia 1 in and out of
water), so the power it gets from the turn takes effect.
The harness needs a still charge the pearl can't miss, so /summon and
saved worlds now load both wind charges, and selectors see them.
Fixes#3299
Co-Authored-By: Claude Opus 5.5 <[email protected]>
piglin.rs: keep main's piglin/hoglin brain memories and add this branch's spear memories to them; the fight activity keeps the spear behaviours before the melee one and main's NotRiding repellent goal; piglin_pose_holding takes both main's passenger flag and the spear.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ai.rs: main's piglin_ambient/hoglin_ambient now pick the angry and retreat sounds from the brain's activity; keep those and add this branch's zoglin angry sound.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on main + this head. I merged main into the branch: the crate split had moved chunk.rs to horizon-client-render, so the LevelShape helpers this PR uses from save.rs are pub now and LevelShape::for_dir sits beside the type (an impl in horizon-client on a type of horizon-client-render no longer compiles). Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (light, levels, distance, worldspawn, portals, chest, forced-levels) pass. chunk_load_bench main vs this branch: same meshes (checksum), mesh time unchanged (42.0–44.2 vs 42.7–43.2 µs per section). Queued to merge; the rest of the level-shape stack follows in order.
On a vanilla or Paper server the controlling passenger's client moves a
saddled horse, donkey, mule or camel (Entity.isLocalInstanceAuthoritative,
LivingEntity.travelRidden), reports it with ServerboundMoveVehiclePacket and
sends START_RIDING_JUMP on a released jump (LocalPlayer.sendRidingJump). The
server only turns it (travelRidden without canSimulateMovement). Horizon's
client only charged the jump bar and waited for the server to move the
horse, so on those servers the horse never moved nor jumped (#2920).
The client now ticks the mount it controls with the rider's input, takes
its synced speed and jump strength, sends its moves and the riding jump, and
puts it back on the server's ClientboundMoveVehiclePacket. Horizon's own
server (single player's server thread, LAN, dedicated) accepts a guest's
MoveVehicle for its horse with handleMoveVehicle's checks, only turns the
horse on its tick, and syncs a horse's rolled jump strength.
Fixes#2920
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server, the client ignored DATA_LIVING_ENTITY_FLAGS for its own
player. When the server stopped the use (an axe hit disabling the raised
shield: Player.disableShield → stopUsingItem), the shield stayed up on
screen while hits landed. LocalPlayer.onSyncedDataUpdated stops a running
use when the flag clears and starts one in the flagged hand when it sets.
Fixes#4115
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A world whose level.dat could not be read (cut short by a crash during a
save, say) was left out of Select World, although level.dat_old would load
it, so the player believed the world was gone.
list_worlds now follows LevelStorageSource.findLevelCandidates and
readLevelSummary: any folder with a level.dat or level.dat_old is a world,
and when level.dat does not read it is listed as a CorruptedLevelSummary
(the folder name, red "Failed to load world summary", "Attempt to Recover";
Edit and Re-Create off). Opening it, like any world whose level.dat does
not read (WorldOpenFlows.openWorldLoadLevelData), shows
RecoverWorldDataScreen with the state of level.dat and level.dat_old
(collectIssue/buildInfo); Attempt to Restore runs restoreLevelDataFromOld
(the broken file kept as level.dat_corrupted_<time>) and ends on the
"Recovery done" confirm or failure alert. Before, opening such a world from
the list could not happen, and opening one with nothing readable would have
started a fresh level in its folder.
Part of #3517
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundDeleteChatPacket (31) was only a constant: Horizon never decoded
it, and the PlayerChat handler dropped the signature a deletion names a
message by, so a line a moderation plugin deleted stayed on screen.
Decode the packet (MessageSignature.Packed), keep the connection's
MessageSignatureCache (unpack the last seen of each PlayerChat, then push it
and the signature, as ClientPacketListener.handlePlayerChat does), and keep
each player message's signature in the chat. handleDeleteChat: a message
still held back by Chat Delay never shows (removeFromDelayedMessageQueue);
a shown one becomes the grey italic chat.deleted_marker line once it has
shown 60 ticks (ChatComponent.deleteMessageOrDelay, with the delayed queue
ChatComponent.tick processes). An unknown packed id disconnects with
multiplayer.disconnect.invalid_packet, as vanilla does.
Fixes#3518
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps both sides: main's world_root and LAN online mode beside the level
shapes, and main's content-identity check before the registry packets,
which now carry the levels' dimension types.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/explosion.rs: main's new interaction parameter for explosion_hurt_entities and this branch's returned player pushes are both kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
gh() and kira() ran subprocess with text=True and no encoding, so on
Windows Python decoded their UTF-8 output with the code page: an issue
body's → came back as →, and merged() wrote it back on the next update.
Follows d3grando's review on #3587.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crate split moved chunk.rs, collisions.rs and world.rs to
horizon-client-render: this commit's changes are reapplied there.
render_tick.rs keeps main's heightmap reads for rain splashes, with the
world's bottom in place of MIN_Y; main's section_mesh_pending (moving
pistons) keys sections from the level's column as well.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A vanilla server never plays an item's break itself:
LivingEntity.onEquippedItemBroken broadcasts the entity event that
entityEventForEquipmentBreak gives for the slot (47 main hand, 48 off
hand, 49 to 52 head to feet, 65 body, 68 saddle) and each client's
LivingEntity.handleEntityEvent runs breakItem on the stack it still has
in that slot. The remote client had no case for these events, so on a
server a breaking pickaxe, armour piece or wolf armour was silent and
left no crumbs, for this player and for everyone else.
Handle them: find the stack in the slot (the own inventory, or the
mob's or other player's equipment), play its break_sound component at
the entity's feet unless the entity is silent, and spawn the five item
crumbs of spawnItemParticles. Entity.DATA_SILENT is now kept for
remote entities so the silent check works.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crate split moved chunk.rs, lod.rs and minimap.rs to
horizon-client-render: this commit's changes are reapplied there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zombie to drowned, husk to zombie and skeleton to stray spawned a fresh,
spawn-finalized mob through MobEvent::SpawnKind with only baby, yaw and
persistence, so the old mob's armour, held items, name tag, effects,
NoAI/Silent and lead were lost (a name-tagged zombie that drowned came
back as an unnamed drowned).
Vanilla's ConversionTracker.doConversion calls Mob.convertTo(type,
ConversionParams.single(this, true, true)): ConversionType.SINGLE and
convertCommon copy all of that onto a mob made without finalizeSpawn,
and Zombie's tracker then runs handleAttributes(special, CONVERSION).
mobs/conversion.rs ports SINGLE + convertCommon, the undead conversions
emit a new MobEvent::Convert carrying the old mob, and Game::convert_to
builds the new mob from it, moves riders onto it and rolls the zombie
attributes. The conversion sound is skipped for a silent mob, as the
level event is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps main's mobs imports in save.rs. LevelShape::for_dir, which the
carried #3481 moved to chunk.rs for the crate split, goes as this
commit removes it: SaveDir::new takes the level's shape instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/fillbiome changed the server's biomes but no client was told: the packet
existed only as an id, the server never sent it and the client had no
handler, so grass, leaves and water kept the old colour until the column
was sent again (single player on its server thread, LAN guests, dedicated
servers).
- horizon-protocol: ChunksBiomes / ChunkBiomeData
(ClientboundChunksBiomesPacket.STREAM_CODEC: ChunkPos long, byteArray
of 2 MiB holding each section's biome container).
- Server: cmd_fillbiome calls Hub::resend_biomes with the changed columns
in FillBiomeCommand's z-then-x order (ChunkMap.resendBiomesForChunks:
one packet per player in the level, with the columns it was sent).
- Client: handleChunksBiomes replaces the loaded column's biomes
(ClientChunkCache.replaceBiomes / LevelChunk.replaceBiomes) and marks
every section of it and its eight neighbours dirty (setSectionDirty).
Part of #3518 (ClientPacketListener.handleChunksBiomes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Frog.updateWalkAnimation overrides the base: the walk target is
min(distance * 25, 1), and 0 while the jump animation is started.
Horizon used the generic min(distance * 4, 1), so a frog's slow steps
barely moved its legs.
Fixes#2848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dismounting used one rule for every mount: a spot either side of the
mount's body, then its back, checking only for full blocks at the feet
and head. Getting off a strider on a lava lake put the rider beside it in
the lava even when the shore was in front of them.
Strider.getDismountLocationForPassenger: walk the columns along the
rider's facing and 22.5 and 45 degrees either side
(Entity.getCollisionHorizontalEscapeVector), from the top of the
strider's box down to half a block below its feet; skip lava, keep blocks
with a valid floor (BlockGetter.getBlockFloorHeight,
DismountHelper.isBlockFloorValid) where a standing, crouching or
crawling box fits (Player.getDismountPoses, DismountHelper.canDismountTo),
and take that pose. With none, land on top of the strider.
The block collision shape and canDismountTo move out of the respawn
anchor's safe-location check so both share them.
Part of #3513.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add four regression tests for issue #3856: client neighbor updates and placement leave TNT alone, server priming is not limited per tick, and a piston duper retains its TNT block while creating exactly one server entity and no client entity.
Verified against nightly-2026-10-29-4baa571 with prepared Java 26.3 resources.
Co-Authored-By: Codex <[email protected]>
The crate split moved gfx.rs, gfx/resident.rs, mesher.rs, streaming.rs and
world.rs to horizon-client-render: this commit's changes are reapplied there.
World::set keeps main's narrower remesh marking (only the sections a write
can reach) with the chunk's own min_y and section count.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Match vanilla TntBlock.prime: client-side piston replay must not create a second primed TNT or remove its block. Server-side TNT duplication remains unchanged.
Refs horizon-rust/horizon#3856.
Co-Authored-By: Codex <[email protected]>
The writer thread sent and checked the keep-alives; a client that stopped
reading blocked it in a write, so it was never timed out and held its slot.
The game thread now checks the keep-alive clock once a second and closes the
socket, which ends the blocked write.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkMap.TrackedEntity.updatePlayer pairs an entity with a player only
when Entity.broadcastToPlayer allows it, and ServerPlayer's override hides
a spectator from every player who is not a spectator (a spectating viewer
sees a player whose camera is its own, which a LAN guest's always is).
The hub's tracker had no such check, so a guest in spectator mode stayed
paired with everyone: its AddEntity, equipment and nametag still reached
the other players, and their crosshair still picked it.
Player states now carry whether the player is a spectator, viewers too,
and the pair check follows broadcastToPlayer. Going in or out of spectator
mode checks the player's pairs again (vanilla's ChunkMap.move does it at
the player's next move), so the others lose it, or get it back with its
armour, at once.
Part of #3520
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The multiplayer connection only had a connect timeout: once joined, a
server that froze or dropped off the network without a TCP reset left
the player in a frozen world forever. Vanilla's client channel starts
with ReadTimeoutHandler(30) (Connection$1.initChannel), so the client
socket now gets a 30 s read timeout.
A lost connection also showed Horizon's raw error text (on Windows the
localized OS message). Connection.exceptionCaught's reasons replace it:
disconnect.timeout ("Timed out") for the read timeout,
disconnect.genericReason ("Internal Exception: ...") for any other
failure, and disconnect.endOfStream when the server closed the stream
(Connection.channelInactive). The integrated server's in-memory pipe
keeps no timeout, as vanilla's local channel has none.
Part of #3520 (Connection#exceptionCaught).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), proof. With your check fix (one loaf each, bites read once they settle), `lan_e2e` `eating` and `eat` pass 2 runs out of 2 on the server thread and 2 out of 2 `--in-frame`. That was in a union with #3706 and #3551 merged into main at a6aa6a071: 111 of 112 affected harnesses passed, none failed, and clippy and the tests passed. Queued.
🤖 Agent: Ludovic's AI agent (Claude), proof. Thanks for taking up the review points (the context now carries only authority and epoch, and the crash paths are documented). This head was in a union with #3674 and #3551 merged into main at a6aa6a071. `pr_ready`: 111 of 112 affected harnesses passed, none failed; the 112th is the LAN suite, which runs by name only. clippy passed, and 1541 tests passed. `lan_e2e` (chest, eat, eating) passes twice on the server thread and twice `--in-frame`. Your container proof covers the dedicated runs and identical saves. Queued.
Player.tick calls setOnGround(false) for any passenger, so the look packets a rider sends (ServerboundMovePlayerPacket.Rot) never carry onGround. Our passenger tick skipped the player's own motion step, so the ground flag from before boarding stayed set and every packet claimed the rider stood on something while the boat bobbed, which Grim flags as NoFall and Simulation.
Fixes#3793
A player who boarded a boat while gliding kept the FallFlying pose: passenger_pose_tick cleared swimming but not fall_flying, so update_player_pose still picked FallFlying and fall_fly_ticks kept counting. Vanilla LivingEntity.updateFallFlying (in aiStep, run by rideTick's tick) clears the glide flag once canGlide fails for a passenger (canGlide needs !isPassenger()), and LivingEntity.tick resets fallFlyTicks when not gliding (LivingEntity.java).
Fixes#3093
backend::remove dropped the .installed marker whenever remove_dir_all reached it,
so a second removal finishing first re-read installed and put the half-deleted
version back in the list that selection_after_removal and Play read.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Since the crate split, chunk.rs is in horizon-client-render while save.rs
(horizon-client) reads min_section, section_count, has_section and
for_dir, so they are pub and for_dir moves out of save.rs's impl block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A camel's legs used the default walk: they kept swinging while it sat or dashed and eased in at 0.4. Camel.updateWalkAnimation aims at 6 times the distance moved (at most 1) only in the standing pose with no dash animation running, otherwise at zero, and eases at 0.2. The remote client's own walk update uses it for camels too.
A leashed sitting camel stayed down however hard the lead pulled. Camel.onElasticLeashPull stands it up when it is sitting, not changing pose, and the standing box fits where it is (canCamelChangePose, wouldNotSuffocateAtTargetPose). AbstractHorse.onElasticLeashPull, which it calls first, also ends a horse's grazing.
A horse that landed from a small hop while still rearing ignored the keys until the rear ended, and a donkey or mule never reared on a jump. AbstractHorse.onPlayerJump sets allowStandSliding and calls standIfPossible (every horse that can rear), getRiddenInput only zeroes the input while the horse is standing on the ground without a jump pending and without allowStandSliding, and tick clears it once the horse stands no more.
A horse, donkey, mule or undead horse that reared kept its rider at the fixed seat. AbstractHorse.getPassengerAttachmentPoint adds (0, 0.15, -0.7) times the previous tick's stand animation (standAnimO) to the seat, turned with the horse. The player and a mob rider both slide.
The rider's seat on a camel was a fixed height for sitting and standing, built from the old full-height box, and a mob rider sat at the top of the box when the camel sat. Camel.getPassengerAttachmentPoint takes the body anchor from the current pose box and lerps it through the sit and stand transitions (getBodyAnchorAnimationYOffset), at the camel's age scale. The player and a mob rider both use it now.
A camel that sat down kept its standing box, so it blocked and was hit as if it still stood. Camel.getDefaultDimensions gives Pose.SITTING an adult box 1.43 lower with eyes at 0.845, and a baby box 0.425 tall with eyes at 0.41. Mob::awake_dims now follows the camel's synced pose, on the server and on a client.
A llama landed with the generic fall sound and hurt on any damaging fall. Llama.causeFallDamage makes no fall sound, and hurts the llama and hands the fall to its riders only when the fall is six blocks or more.
A rider took no damage when the mob under it landed hard, and a mob on a mount's back never felt it either. Entity.propagateFallToPassengers hands the fall to every passenger (LivingEntity.causeFallDamage first, AbstractHorse.causeFallDamage after its own hurt and only when hurt), and each passenger runs its own causeFallDamage with its own safe distance. A mount now raises a fall event the game turns into Player.causeFallDamage for the local player and server players, and the mob causeFallDamage for a mob rider.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch (eb3f03d7f, a merge commit, fast-forward). The only conflict was `lib.rs`'s re-export list: `section_mask` now sits beside main's `potent_sulfur` and `split_layout`. The full proof the orchestrator asked for, on this head merged into today's main: - the 121 vanilla harnesses this branch can reach pass, and `dedicated_server` too; - pr_ready passes; - lan_e2e passes on the server-thread host: light, levels, distance, portals, chest, forced-levels and worldspawn. In-frame is deprecated now. worldspawn failed once on timing, then passed 2/2. Speed: the masks only grow from 4 to 32 bytes, in per-section bookkeeping (dirty sections, the light log, LOD detail columns). There is no allocation and no per-block work, so frame and tick costs stay the same. Queued for merge. #3481 comes next, once this one is in.
A creative click on a server changed the local slot but sent it at the end of the tick, while a thrown stack went out at once. A slot update the server sent in between (the pickup of an item a held Q had thrown) replaced the unsent change and was marked as synced, so the throw was never taken off the stack and the count climbed. CreativeModeInventoryScreen.slotClicked ends in broadcastChanges (CreativeInventoryListener.slotChanged -> handleCreativeModeItemAdd), so the changed slots follow the click immediately; Horizon now sends them right behind it.
Fixes#3090
Checking a world on the title screen takes the session lock for an instant; hand it back explicitly, as SessionLock does, so a quit world is never listed as locked. Drop the stale note that powder snow climbing is not ported for rabbits.
🤖 Agent: Ludovic's AI agent (Claude), proof. The real NeoForge pack test above covers this PR's exporter. On a 3-mod pack it wasn't developed against, the exported item facts matched 52 of 52 values read with vanilla's own commands on the same server, without the exporter. CI verify is green. Queued, first in the stack's order.
🤖 Agent: verified by server-tick-1. A chunk worker idle for 5 s frees its generator's density scratch and lookup caches (caches only, results unchanged), and the last idle worker returns freed memory to the OS once; −71 MB idle RSS on Linux. pr_ready green.
frog.rs: keep the goat jump cooldown and main's vanilla memory id long_jump_cooling_down.
remote_mob_data.rs: keep the tadpole AGE_LOCKED line beside main's mooshroom comment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by server-tick-1, after merging main. ServerAdvancements and the game's criteria share one advancement tree (Tree::shared, Weak-held) when the world's packs bring no advancement files; with pack advancements each loads its own, as main does. −9 MB idle RSS on Linux. pr_ready green.
The known issues still said C/X aren't wired to the hotbar loader; they are
since 85f34393f (game/creative_hotbar.rs), and #3693 added the Saved Hotbars tab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#3582 and #3748 fixed the same bug (#3614) twice. The server now sends the
command only through `lan_open_command_block` (ServerPlayer.openCommandBlock),
no longer in every chunk and block update (CommandBlockEntity keeps the empty
getUpdateTag), and the open edit screen refreshes its fields in one place
(CommandBlockEditScreen.updateGui). docs/rust-parity.md: the toolbar keys and
the Saved Hotbars tab are wired since #3693.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The per-player inbox had no limit: a client sending 10 000 packets a tick
grew the server by about 1 GB a minute (#4179).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Proof of this head merged into today's main (101d3b0be, which now has #3300, #2698 and #3802): - the 108 vanilla harnesses this branch can reach pass, `dispenser` and `dedicated_server` included; - pr_ready passes; - lan_e2e passes: worldspawn, forced-levels, distance, spawned, levels, light, mobs. Queued for merge.
🤖 Agent: Ludovic's AI agent (Claude), triage. #3712 landed with a mob_ai case of its own, so I merged main into your branch again (no rebase). The only conflict was the case list in `tools/test_mob_ai_parity.py`, and both sides are kept. `parity.py mob_ai` passes on main with this and #3721/#3735 merged. Requeued.
🤖 Agent: verified by server-tick-1. Worldgen binds the feature block tags once (GeneratorData::feature_block_bindings) and shares them through Arcs; the maps' readers are unchanged, so decoration gives the same result. −11.6 MB allocated on the dedicated server. pr_ready green; parity 152/153, the failing dispenser case comes from main (golem PRs #4022/#3959), not from this change.
🤖 Agent: merging DUzzL's campfire contact damage for mobs (tested in game on both single-player paths: armadillos, cows and sheep burn on lit campfires, Infested spawns silverfish). Thanks!
🤖 Agent: Ludovic's AI agent (Claude), triage. #3729 landed with mob_ai cases of its own. I merged main into your branch (no rebase). The only conflict was where the new cases go in `tools/test_mob_ai_parity.py`, and both sets are kept. The Rust merged clean, so the proof above still holds. Requeued at the merged head.
🤖 Agent: Ludovic's AI agent (Claude), proof. This PR was in a union of five mob PRs (#3712, #3721, #3729, #3735, #3773) merged into current main. `parity.py --changed origin/main`: 55 of 55 affected harnesses passed, `mob_ai` included. `horizon-client` lib tests: 726 passed. clippy `-D warnings` is clean. The only conflicts were the new cases in `tools/test_mob_ai_parity.py`, as resolved on your branch. Queued.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Since sirrayi's review, I merged main into this branch twice; the last merge is 655b331b1. In the merges: - `VanillaTerrain` keeps main's world presets (2607d06f9). This branch only adds `region_random` and `spawn_generated`, and its tests call `for_dimension` without the preset argument. - Main's goat and frog code (`goat_init_memories`, the goat ram cooldown, the frog's long jump) used main's memory names, while this branch saves vanilla's ids. Both now use vanilla's: `long_jump_cooling_down` and `ram_cooldown_ticks`. - main's mooshroom kind test sits beside #2544's baby villager test. Tested on today's main: - With #3802, #4095 and #3300 together: 123 of the 125 harnesses this branch can reach pass, along with pr_ready and lan_e2e (mobs, spawned, worldspawn, drops). The 2 failures don't come from this branch: `dispenser` passes on main plus this branch alone, and `command_exec` failed because the vanilla server was under load. - On this head alone: `dispenser`, `finalize_spawn`, `natural_spawn` and `mob_ai` pass, and the 48 harnesses that cover goat.rs, frog.rs and finalize.rs pass 48/48. Queued for merge. When it merges, GitHub marks @FusselTV's #2544 and #2586 as merged.
The eater held the use for 2.2 s with two loaves: the held use started
the second loaf and let go of it a few ticks in, around its second bite
(Consumable.shouldEmitParticlesAndSounds: every 4 ticks once 7 of 32 are
past). That bite reaches the watcher from the server, whose use runs on
its own ticks behind the eater's client, so read 10 ticks after the hold
the watcher could still be one short (9 of 10, only when the eater drew
that bite).
Each player now eats a single loaf, so the count is the loaf's 6 bites
plus the last bite and FoodProperties' sound, and both sides' counts
are read once neither has changed for 20 ticks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The download queue's HTTP agent had no connect or read timeout, so a server
that accepted the connection and went silent blocked the single queue thread
and the pack screen forever. Fail the request after 30s instead.
Fixes#4168
🤖 Agent: verified by server-tick-1: a spawner asks for an alive player in range before the spawn level is built, as BaseSpawner.serverTick returns first thing without one (no draw, no change); natural_level 1.6 % → 0.5 % of the tick thread at 100 bots; verify green, clippy -p horizon-client clean (main's log_display and axolotl test failures are main's); merging once CI parity passes.
🤖 Agent: queuing the lan_e2e levels test fix (#4145): the End gateway's ExactTeleport key and closing the guest's credits; levels passes on main's binary, pr_ready green.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. @masa-masa-0314, this PR is on the maintainer's priority list, so instead of asking you again I merged main into your branch myself. Your commits are not rewritten; it took three main merges today, the last one at ada669266. In the conflicts: - main's new code (cushions, crafted-recipe triggers, creative slots, item use) now reads the world's tables through your `self.items()`, `world_items!`, `world_stations!` and `world_recipes!`; - main's new tests pass `PackOverlay::NONE`; - `game/mod_render.rs` stays deleted, as main removed the wasm mod UI. Tested together with #3802, #4095 and #2698 on today's main: - 123 of the 125 vanilla harnesses this branch can reach pass. `dispenser` fails because of another PR in that set; it passes on main plus this PR alone. `command_exec`'s failure was the vanilla server under load; on this head it matches 902/902. - pr_ready passes. - lan_e2e passes (crafting, recipes, drops, mobs, spawned, cushions). Queued for merge. Thank you for the data-pack work!
Main's goat_init_memories, goat ram and frog jump code keep their brain
memories under main's names; this branch saves them under vanilla's ids.
Both now use vanilla's: long_jump_cooling_down and ram_cooldown_ticks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: queuing the portal trip fixes (Discord: Nether portal overlay stuck after a dimension change; End portal leaving the player in the air): the overlay fades in the arrival portal, and the player stays on the loading screen until the exit is found. Tested in game on both paths; lan_e2e portals passes. pr_ready is green except dispenser, main's regression from #3959 fixed by #4120.
🤖 Agent: Ludovic's AI agent (Claude), proof. This PR was in a union of 16 needs-proof PRs merged into main at 1dde830ba. Parity: 195 of 196 affected harnesses passed, none failed. clippy and rustfmt passed. `lan_e2e --check commands` passes on the server thread and `--in-frame`. The command block's edit screen refreshing from the server's data is covered there. Queued.
With mimalloc's default 1 s purge delay the server held 26% and the client
37% more memory than with the system heap. Purging at once keeps the server
within 3% and the client within 9%, and generation keeps most of its speed-up
(+18% chunks a second on 24 workers, +3% on one).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
World generation, meshing and the tick allocate and free small blocks on
every worker at once, and the system heap serialised them: on Windows
RtlAllocateHeap/RtlFreeHeap were 16% of chunkgen_bench's CPU. mimalloc keeps
a heap per thread. release_free_memory also gives mimalloc's free pages back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by server-tick-1. Stacked on #4009: a dedicated server keeps the block models and sprite layers but no texture pixels or animation frames, which only the renderer reads; −28 MB idle RSS on Linux. dedicated_server passes locally; merge after #4009.
The End gateway's exact flag is `ExactTeleport` in TheEndGatewayBlockEntity's
NBT; `exact_teleport` was ignored, so the gateway searched for a landing and
left the guest away from the spawn platform. The guest's first exit from the
End then rolls the credits (ServerPlayer.showEndCredits) and only closing
them takes it home, as the portals step already does for the host.
Fixes#4145
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creative.rs, with #3693's Saved Hotbars tab: Tab gains hotbar, set in Exported and Tab::header. Neither side's tab skip is kept: the hotbar tab now shows (#3693), and an empty category tab, the operator tab without permissions among them, is left out by its stacks after the permission filter (this branch), so the exporter's visible flag is no longer read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's AbstractContainerScreen.isHovering grows each 16x16 slot by one
pixel on every side; slot_at used the bare 16x16 box, so the 2px gaps
between slots selected nothing. Fixes#4134.
Its late acceptance set the player's position back to the old level's
coordinates, so a trip that started right after a teleport waited forever for
chunks around the exit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each streaming worker keeps its generator's density scratch buffers and
lookup caches between jobs, megabytes per worker, for as long as the world
is open: about 51 MB on the Mac mini's dedicated server with nobody on. A
worker that has waited 5 s with an empty queue now frees them (and the
mesher's face cache); the next job makes them again. Results do not change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/server_packs.rs: main's WORLD_PACK_ID is kept; PackReload and RELOAD_LINES stay in game/packs.rs, where this branch moved them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without the teleport change_level used to send at once, moves the client had
sent from the old level were taken as positions in the new one, and the level
loaded its chunks there instead of around the exit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A crystal spawned from an add-entity packet kept time 0, so every crystal
placed in multiplayer or LAN looked identically rotated. Vanilla's
EndCrystal constructor draws time from nextInt(100000).
/attribute reported the base attack damage, attack speed and reach with a
weapon in hand because apply_equipment skipped the main-hand slot. Apply
them and refresh when the held item changes. Fixes#4118.
MerchantMenu.playTradeSound plays the trader's notify-trade sound
(villager or wandering trader), but the game dropped the queued
TradeSound event. Play it at the trade's origin with volume and pitch 1.
Part of #3515.
A chunk's summary skipped blocks that neither draw solid nor collide as a
full block. Kelp and seagrass reaching the surface left holes in the far
sea, and patches of powder snow left holes in mountains (paths, farmland
and stairs dips).
- Blocks holding water that aren't kept (kelp, seagrass, bubble columns,
waterlogged ones) are drawn as water, so the water is one span.
- Powder snow, dirt paths, farmland, honey blocks and stairs are kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012gFJzABp1uePB4gm32PuaC
native_set_block_property (a helper, API 3): as a furnace's tick sets
its lit, so a machine shows when it works.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), pr-triage-3. Harness and horizon-ctl runs no longer add the official server to servers.dat in the working directory (the repo root) or send the players ping; before/after checked with a fixture run in a scratch folder. Queued.
Read the player's minecraft:block_break_speed attribute and multiply the destroy speed by it in both the server mining tick and the client's destroy-progress prediction, matching Player.getDestroySpeed.
Fixes#4094.
A remote player or mannequin whose skin location had no atlas sprite
(a stored slot without its upload, an evicted skin) drew no body. The
model now falls back to the default skin texture, and a skin counts as
stored only when its sprite is in the atlas, so it is stored again.
Refs #4096
A content mod's textures/gui files are GUI sprites under its namespace
(mymod:gui/...). On the client, native_screen lays out a mod block's menu
screen (height, slot positions, the inventory's row) and
native_screen_draw draws it, over a plain panel with the slots' frames,
from the menu's data slots, which the client now keeps for it; the
helpers native_sprite and native_sprite_size draw any part of a sprite.
The server's menu is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The official server's servers.dat entry and its marker go into the game
folder, which is the working directory, and the "N playing now" ping
starts at the same place. The guard skipped servers, benchmarks,
offscreen and frame-limited runs, but not a harness's fixture
(HORIZON_DISPENSE_FIXTURE, HORIZON_SCULK_FIXTURE, HORIZON_COMMAND_SCRIPT,
HORIZON_NET_REPORT) or a run horizon-ctl drives. Harnesses start those
in the source checkout, so a parity run left servers.dat and
horizon_official_server.txt in the repo root and counted itself as a
player. The guard now reuses synchronous_startup, which already names
every one of these runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerAdvancements (the advancement packets a server sends) and the game
that runs the criteria each read and parsed every advancement file into a
tree of their own: 7 MB each, the same twice. Tree::shared keeps one per
data folder while anyone holds it, and both use it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The area_effect_cloud entity states are built while the LAN hub is out of
`self.lan` (tick_lan takes it), so encoding the potion cloud's particle
through `network_particle` always returned None and DATA_PARTICLE was
never sent. Clients then fell back to AreaEffectCloud.DEFAULT_PARTICLE, the
white entity effect, for every potion. Encode it through the hub passed to
lan_projectile_states instead, as the dragon breath particle already is.
Closes#4090
PathNavigation.moveTo trims the path first and refuses it when the trim
leaves no node. The trim was deferred to the next nav_tick, so move_to
answered true for a tick and goals started on a path that was already empty.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. Wither skeleton: its piglin target goal comes first among the priority 3 goals (vanilla's registerGoals order). levzzz5154's main merge touched only the mob_ai case list; the Rust change is the same as on the CI-parity-green head, and verify is green. Requeued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Far terrain: no dark band in the sea where chunks were loaded: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Telemetry: a user named "user" keeps the <user> label whole: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Data packs: a pack's block state providers join the state_providers bundle: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Let thrown pearls hit wind charges: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
The Skin Customization Main Hand setting was saved to options.txt and
applied to the local hand, but the ClientInformation packet sent at join
always hardcoded main_hand = 1 (right). The server therefore never
learned the choice, so mirrored and server-side arm visuals stayed on the
right.
Session setup now receives the ClientInformation built from the settings
(Settings::client_information), the same value already used for the
resend-on-change path. Adds round-trip and mapping tests (LEFT = 0,
RIGHT = 1).
🤖 Agent: WebAssembly removal 4/4 (docs), on #3353, merged up to date with main (roadmap: main's, with the native-mod wording reapplied; docs-only on top of the union proven in proven.txt). Merge after #3353; horizon-site#6 follows.
Main now builds a world's preset itself (2607d06f9): VanillaTerrain keeps
main's for_world/for_world_with signatures, this branch adds region_random
and spawn_generated beside them, and its tests call for_dimension without
the preset argument.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. Stamp DataVersion only after the world data is upgraded: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Fix camel freeze after spawn and LastPoseTick saved as a sign: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
canSpawnGolem (a dispenser's carved pumpkin) and the setBlock that places a
head ask before the head is there, with vanilla's base patterns (" ", "#",
"#") and ("~ ~", "###", "~#~"), whose head cell takes any block. #3959's
find_pattern required the head there too, so a dispenser no longer built a
snow or iron golem: dispenser parity failed on main (carved_pumpkin, 1 of
6000 cases).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's new code reads the world's tables (self.items(), world_stations!,
world_recipes!); main's new tests pass PackOverlay::NONE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla loads entity sections on other threads, so on a loaded runner
400 ticks (~2 s) ran out before the chunks around the player were
entity-ticking: the oracle threw "entities around the player did not
load" and main's parity has failed on command_exec since 2026-10-29 22:05.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's feline_server_ai_step sets the cat's sprint and Mob::movement_speed
adds the +30% for it, so the cat arm of custom_server_ai_step, the sprint dust
draws and the speed_bonus arm (a second +30%) are dropped; the avoidance goal
order and the CatAvoidEntityGoal probe name stay.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla finds a portal's exit (the End platform, the Nether portal, the spawn
column after the End) before ServerPlayer.teleport sends the Respawn packet.
Horizon finds it once the destination has loaded, and meanwhile showed the
client at the approximate point: in the air above the End's void or at the
raw world spawn. The client now gets its position, LEVEL_CHUNKS_LOAD_START
and the portal sound only when the trip lands, so it stays on its level
loading screen as in vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: WebAssembly removal 3/4 (launcher: a WebAssembly mod needs a native port), on #3351; proven with #3351 and #3371 on union/wasm-removal (proven.txt). Launcher clippy and its 275 tests pass. Merge after #3351.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into your branch (a fast-forward merge commit, 0e4f8e279; none of your commits rewritten). Since this PR was opened, main changed the HDR parameters (black level, UI brightness, tiers: 20 floats) and screenshots (`ScreenshotRequest`, world icons). In the merge: - `Hdr::params` fills main's layout for both the swapchain and your HDR10 capture; - only full-frame screenshots get the `-hdr.png` copy, not world icons; - the docs sentence moved into main's HDR paragraph. Tested on this head merged into main: - The harnesses it can reach pass (59/60). The one failure, `command_exec`'s `/data get` `custom_name`, comes from main. - fmt, clippy and the render tests pass. - On macOS (HDR10 output), a screenshot wrote both PNGs: the HDR one is 16-bit RGB with cICP 9/16/0/1 and cLLI 1000/203. Queued for merge.
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: release notes always link horizon-rust/horizon, even when a fork's CI builds them. verify is green. Queued.
🤖 Agent: WebAssembly removal 2/4 (crates: runtime, SDK, official WebAssembly mods), on #3350, merged up to date with main c7065027b. Workspace compiles (--all-targets --locked). Merge after #3350.
The roadmap: main's (Now at most 8 items) with this layer's native-mod
wording reapplied; the WebAssembly runtime is removed, not being removed.
docs/README.md: main's data pack upgrade line kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture's CATEGORY tab was empty, and an empty category tab is now
hidden (CreativeModeTab.shouldDisplay), so tab 0 was no longer the tab the
test selects. It now holds a stone stack.
Part of #3393.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. Docs only: a server.properties → server.toml page for owners, linked from docs/README. verify is green. Queued.
native_mod_block_entity_at (a helper, API 3): the mod block entity at a
position, to read or change from another's tick, as a generator charging
the machine beside it. As setChanged would, its chunk is saved next time
and comparators read it again; a tick that moved its own items tells
comparators too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mobs/ai.rs: this branch's same_vehicle_push_tests and main's tests appended after it are both kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
handleRespawn gives the new LocalPlayer the server's portalCooldown, and the
client's setAsInsidePortal only restarts it while the player stands in the
arrival portal. Horizon sent 0 and ignored it, so the purple overlay built up
again and stayed on screen after a dimension change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decoration, structure placement and live features each built their own
FeatureBlockBindings, binding every block and every block tag to its state
set: 5.5 MB each, the same three times. GeneratorData now binds them once
(feature_block_bindings) and the bindings share the maps through Arcs. Each
decorated region also cloned the whole tag map; it now shares it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review follow-up: as a ticking block entity's setChanged does, so a save
after an autosave still writes what the tick changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod container may open a furnace's menu (furnace, blast furnace,
smoker): its block entity's data slots, which the mod sets in its tick and
nothing saves, draw the flame and the arrow as a furnace's dataAccess
does. The tick hook now takes the block entity whole (id, nbt, items,
data), so fields added later don't change its signature.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new fn sat between selection_after_removal's doc comment and that
function, so the comment documented the wrong one.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
The shading was a rough guess (front 1.0, top 0.4); it is now
minecraft_mix_light with the two inventory lights, ambient 0.4 and power 0.6.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Projectile::tick chose the water inertia (0.8) from the fluid of the block
at the projectile's feet, every tick including the first. Vanilla's
ThrowableProjectile.applyInertia and AbstractHurtingProjectile.applyInertia
read Entity.isInWater (wasTouchingWater), which the previous tick's
Entity.baseTick -> updateFluidInteraction set from the deflated bounding
box after the move (EntityFluidInteraction.update: some water surface at or
above the box's bottom), and which is still false on the first tick.
So a pearl thrown under water lost a fifth of its speed on the first tick
(it lands about 6.0 blocks away instead of 7.4), one skimming just above a
pool slowed as if under water, and one whose box dipped into the next
column's water did not slow at all.
The projectile now keeps touching_water from the box after each move
(ProjWorld reads it through MobWorld::fluids, the mobs' port of the same
test); llama spit, whose base tick runs before its move, tests its box at
the start of the tick. Client-side arrows test their box too.
Part of #3072.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs spawned while the game runs (/summon, spawn eggs, spawners, natural
spawns, raids) go through the reduced spawning::finalize, which skipped
Mob.finalizeSpawn and WitherSkeleton.finalizeSpawn:
- no mob got the random_spawn_bonus follow range modifier, and none was
ever left-handed (vanilla: 5 percent);
- wither skeletons kept the default 2.0 attack damage instead of 4.0.
Move the Mob.finalizeSpawn body into finalize::mob_spawn, shared by the
worldgen Ctx::mob, the summoned iron golem and now the runtime finalize
(every mob but the cave spider, whose finalizeSpawn does not call super),
and set the wither skeleton's attack damage base to 4.0. A bred baby
(Animal.spawnChildFromBreeding: getBreedOffspring runs no finalizeSpawn)
drops the bonus and the left hand again, as it already drops the sound
variant.
Part of #2783.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A wolf's wet and shake state lived only in the server's ai_step, so a
client of a server (the default single player on its server thread, a
LAN guest) never saw a wolf darken in water or rain, nor shake dry.
Split Wolf.tick's wet and shaking part out so both sides run it: the
client knows on its own when the wolf is in water or rain (isWet), and
follows the server's entity events 8 (start shaking, broadcast by
Wolf.aiStep) and 56 (cancel, from Wolf.tick back in water or rain) in
Wolf.handleEntityEvent. The shake now starts and plays its sound in the
same tick as vanilla, and the splash offsets come from the wolf's random
on both sides, as Wolf.tick draws them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FlatLevelGeneratorSettings.adjustGenerationSettings decorates a flat
world with its biome's placed features when `features` is on and the
world isn't all air, or its biome is the_void. The Void preset (the_void,
features, one air layer) so places the_void's only feature,
void_start_platform: a 33x33 stone square with cobblestone at 8, -61, 8.
FlatTerrain placed only the layers, so players fell into the void.
Biomes now keep each biome's `features` by step, FlatTerrain works out
the adjusted list and places void_start_platform (the ported
VoidStartPlatformFeature) when it is there. The other features, lakes
and structures are still not placed (#2912).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FirstPersonHandsAndItems.shouldInstantlyReplaceVisibleItem keeps the
visible item when it is the same ItemStack object, or one that
matchesIgnoringComponents (count, item and components, with damage
ignored). Horizon stood in for the object by the slot, so the off hand,
always the same slot, kept showing a stack of the same item that the
swap key had replaced, whatever its count (#2866).
A swap now marks both hands as holding new stacks: at once in the local
game, and on a server (the thread one included) when the slot updates
that answer SWAP_ITEM_WITH_OFFHAND arrive. Those hands then compare by
matchesIgnoringComponents until they show the held stack again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The option was in Options > Controls and saved, but nothing read it.
MacosUtil.setCtrlClickEmulatesRightClick makes the click a right click, and
its release too.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ModContainer.menu names a plain slot grid (hopper, generic_3x3,
generic_9x1..9x6) with as many slots; using the block opens it over the
block entity's slots, through the menu code vanilla's containers use, titled
with the block's name (Block.getDescriptionId).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockEntityRenderer loaded occlusion.nbt and the render definitions a second
time and built its own FaceVisibility, whose shapes then computed their faces
again: 13.6 MB of face shapes, twice. It now takes a clone of the mesh
tables' visibility, which shares the shapes and their cached faces.
Probes and tests that load the renderer alone still load their own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu hides every adapter whose driver reports Vulkan conformance version 0
unless `ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER` is set, and Mesa reports
0.0.0.0 for hasvk, which drives Ivy Bridge and Haswell. Such a GPU is told
it has no driver while Vulkan runs, and the only escape was the player's
environment variable.
`open_device` now asks a backend that offered no adapter at all a second
time with those adapters shown, and says in the log which adapter it took.
Every machine where a backend already gives an adapter is untouched.
Fixes#4059
🤖 Agent: A Java instance's Home no longer shows Horizon's version picker or nightly updates; its chip names its own version and loader. Checked in the real launcher (scratch homes, Fabric Java instance and Horizon instance), launcher fmt/clippy/278 tests pass on this head.
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: `npx` is resolved by path (`npx.cmd` on Windows), with a clear error when it's missing. verify is green. Queued.
CampfireBlock.entityInside hurts any living entity in a lit campfire for 1 (2 for soul campfires) with the campfire damage type, which is fire, makes mobs panic and does not knock back. Mobs ignored campfires entirely.
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Thanks for the fix: with the wither held in place the scenario now matches vanilla. On this head (f5de793b3) merged into current main, the harnesses this change can reach pass: `command_exec`'s wither spawn (4/4), wither breaks (9/9) and ender dragon (5/5) groups match vanilla. Its 2 remaining mismatches are `/data get` printing a `custom_name` as a component; they come from main and this PR doesn't touch them. fmt, clippy and the horizon-client tests pass too. Queued for merge.
Game::glint() and glint_ticks() read the clock and looked glintSpeed and
glintStrength up by name (a linear scan of the options table) for every
item submitted, so a frame with thousands of dropped items read the clock
thousands of times. Vanilla snapshots the options once per frame
(GameRenderer.extract -> extractOptions into OptionsRenderState) and
TextureTransform.setupGlintTexturing reads Util.getMillis() once per glint
render-type batch. Capture a GlintFrame at the start of each render() and
let world items, armour, held items and GUI items read it.
Fixes#3183
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Home drew Horizon's version picker ("No version" or a nightly) beside a Java instance's Play,
and offered Horizon's next nightly in its hero. A Java instance now shows neither: its chip
says its Minecraft version and loader ("Minecraft 1.21.1 · Fabric 0.16.9").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.push(Entity) never moves an entity that isVehicle, so in vanilla
a camel carrying a husk is not pushed by a zombie either. The test now
checks only that the camel still pushes the zombie.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The third-person model dropped its interpolation whenever it moved more
than 10 blocks in a tick, while the camera kept lerping from the old
position. Levitation 254/255 rises about 12 blocks a tick, so the body
sat a whole tick ahead of the camera and left the frame.
The model now lerps from the player's own old position, as vanilla's
EntityRenderer.extractRenderState does from Entity.xOld/yOld/zOld, the
same one Camera.setup uses. Teleports and respawns already reset that
old position, so they still jump without a sweep.
Fixes#3616
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seed the existing bounded request path with planned retiring leaves still missing saved detail, so full-detail geometry can retire after its replacement is ready.
Fixes#4039
Co-Authored-By: Codex (GPT-6) <[email protected]>
A mod block's block entity gets item slots when its mod says the block has
a container (native_block_entity_container: the slot count, the slots each
face reaches, and which slots take input or give output, as a
WorldlyContainer). Hoppers, droppers and comparators use them through the
container code vanilla's blocks use, breaking the block drops them
(Containers.dropContents), they save as Items, and the tick hook gets them.
Fixes#4034.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. Removes `ctov.rs`'s modular_compat code, which nothing calls any more (lithostitched owns that processor), with no change in behaviour. verify is green. Queued.
The Operator Items Tab option was greyed out and nothing read it: the
creative export was built without permissions, so the operator tab had no
stacks and was dropped.
VanillaCreativeTabsExporter now builds the tabs with permissions and marks
the stacks only that build lists (permissions_only). Creative keeps every
exported stack and rebuilds its tabs and search tab for hasPermissions
(canUseGameMasterBlocks and the option), as CreativeModeInventoryScreen
does on opening and in containerTick (tryRefreshInvalidatedTabs): a
selected operator tab left empty gives way to the default tab. The option
is switchable again. test_creative_parity.py runs with and without
permissions.
Part of #3393.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. Main gained another mob_ai case beside yours, so I merged main into your branch (no rebase). The only conflict was the new cases' place in `tools/test_mob_ai_parity.py`, and both are kept. Requeued at the merged head.
ArmorStandItem.useOn returns SUCCESS once the stand fits (FAIL on a bottom
face or when something is in its box), and SUCCESS swings the hand. The
local placement now swings, and a server's client runs the same check in
startPrediction, before UseItemOn goes out, and swings when it holds.
armor_stand_spawn holds the checks both share.
Fixes#4024
Co-Authored-By: Claude Opus 5.5 <[email protected]>
KnockbackClient records the push each ClientboundExplodePacket carries;
two cases put Bob in creative two blocks from an end crystal he hits and
from primed TNT.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerExplosion.hurtEntities puts each pushed player in hitPlayers and
ServerLevel.explode sends them that push in ClientboundExplodePacket. Horizon
pushed the server's copy only and sent it to the client with a hurt's motion
packet, which a creative player never gets: a crystal they hit left them
standing, and TNT replaced their motion with the server copy's. The pushes
now go in the explosion packet (deferred with it in a guest's turn), except
for a hurt player, whose motion packet already carries the push.
Fixes#2755 (its survival part was fixed by #2730).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hdr::params now fills main's 20-float layout for both the swapchain and the
HDR10 capture; the capture keeps its own pipeline and parameters. Screenshots
use main's ScreenshotRequest and copy_frame; only full-frame requests (not
world icons) get the -hdr.png copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A row or tab that isn't hovered no longer builds its (invisible) hover plate, and plate::cut
paints its rectangles in one canvas instead of a div each: the sidebar and the top bar redraw
on every sky frame, so they cost a few elements instead of hundreds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.canStartSprinting refuses while isSlowDueToUsingItem (an item in
use whose use_effects do not set can_sprint), and LocalPlayer.aiStep zeroes
the double-tap window while it holds. The sprint phase had the condition but
the player never fed it, so the sprint key or a double-tap of forward could
start a sprint with a shield up, which vanilla does not allow (measured: 0.055
blocks per tick against 0.043 for the shielded walk). Items now carry
use_effects.can_sprint (spears set it) and the window passes the state to the
sprint phase.
Fixes#3422
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
The owner's sit toggle on tamed wolves and cats only stood aside for food
an adult could breed with (age 0, not in love), so fish or meat on a tamed
baby flipped it between sitting and standing and it never grew.
Wolf.mobInteract and Cat.mobInteract call TamableAnimal/Animal.mobInteract
first and toggle setOrderedToSit only when that did not consume the click;
Animal.mobInteract takes the food from an adult that canFallInLove at age 0
or from a baby that canAgeUp (AgeableMob.canAgeUp: isBaby and not
isAgeLocked), growing it by getSpeedUpSecondsWhenFeeding.
mobs::ai::takes_food is that test, shared by the sit guards and
feed_animal (which now also leaves age-locked babies alone, as canAgeUp
does).
Part of #2780.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderDragon.knockBack pushes the server's copy of every player in a wing's
zone each tick, but a player's own client only receives its velocity when a
hurt marks it (LivingEntity.hurtServer's markHurt, sent by
ServerEntity.sendChanges). Horizon added the push straight to the player's
velocity on every tick they stayed in the zone, perched dragon or not, so
players were flung several blocks per tick around the exit portal.
The push now goes out only with a 5-damage wing hit that lands outside the
hurt cooldown: never under a perched dragon (isSitting), and not within 2
ticks of a mob's hurt (getLastHurtByMobTimestamp() < tickCount - 2). The
vertical push is 0.2f widened, as in vanilla.
Fixes#2893
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.push returns at once for isPassengerOfSameVehicle, but
EntitySelector.pushableBy does not drop such a mob: it still counts for
cramming in LivingEntity.pushEntities, and Warden.doPush still runs for
it before Entity.push. push_entities now skips only the push itself.
A unit test for each half of #3199 (a camel and its husk never push each
other; RandomSitting never starts with a husk on its back or with a
lead, and does with nothing or a slime there), with a shared test world
builder in mobs/world.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.pushEntities pushed every pushable mob overlapping the box,
the rider and its own vehicle included. A husk sits half a block toward
the camel's front (Camel.getPassengerAttachmentPoint), inside the camel's
box, so each tick it pushed the camel backwards and the camel pushed
back: the camel slid off tail first and kept speeding up. Entity.push
skips passengers of the same vehicle (isPassengerOfSameVehicle: the same
root vehicle); pushEntities now does too, through the vehicle every Seen
mob now carries.
CamelAi.RandomSitting also checked only for a player on the camel's back.
Vanilla needs no controlling passenger at all (hasControllingPassenger),
and a husk is one (Mob.getControllingPassenger: a mob rider outside
#non_controlling_rider), so a camel carrying a husk sat down at night with
the husk floating above it. The goal now checks the mob rider, and the
leash vanilla's checkExtraStartConditions also checks.
Fixes#3199
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BegGoal sets Wolf.DATA_INTERESTED_ID on the server, but the LAN host
never sent entry 20 and a client never read it, so every window
(single player joins its server thread the same way) kept the head
straight. Send the flag with its default, read it into the remote
wolf, and run the part of Wolf.tick both sides run on the client:
interestedAngle eases 0.4 of the way toward the flag each tick, which
getHeadRollAngle turns into the head's roll.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundPlayerLookAtPacket was decoded as an unknown packet, so
`/teleport ... facing` and `/rotate ... facing` on a server never turned
the camera. Decode it and port ClientPacketListener.handleLookAt:
Entity.lookAt from the player's feet or eyes towards the packet's entity
(its feet or eyes) when the client tracks it, else towards the point.
Part of #3518.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
head_item_geometry now draws the local player's and other players' head
items; main's player_pumpkin_geometry stays only for LAN guests drawn on
the in-frame host, whose models carry no worn stacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. `travelInWater` (the 0.8 slowdown, then `getFluidFallingAdjustedMovement` with gravity/16) and the bubble column push for armour stands match vanilla. The per-tick water scan only covers the stand's own box. CI parity is green at this head. Queued.
Fixes#3531's render side landed in #3341; this adds the shadow cases its
branch carried for tools/test_text_display_render_parity.py so the
packed ARGB ints, the [r, g, b, a] lists and NO_SHADOW=0 are checked
against the vanilla TextDisplayRenderer oracle.
🤖 Agent: Ludovic's AI agent (Claude), triage. `WaterAnimal` and `AgeableWaterCreature` set the WATER malus to 0, and FishMoveControl's yaw uses `Mth.atan2` in degrees. CI parity (mob_ai with new fish cases) is green at this head. Queued.
A dedicated server loads the block models and the sprites' layers, as before,
but no longer lays out the texture array's pixels or the animations' frames:
nothing draws them. The integrated server shares the window's resources and
is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic's AI agent (Claude), triage. `WitherBoss.hurtServer` ignores damage whose source entity is in `#wither_friends` (the undead), for explosions and wither skulls alike. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Sleeping now follows the sky darkening (`skyDarken >= 4`, `isDarkOutside`) instead of a fixed time window or the thunder flag alone, so a thunderstorm at noon allows it and rain alone doesn't. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. `MinecartFurnace.push` is a Vec3, and `calculateNewPushAlong` (`projectedOn(...).normalize().scale(push.length())`) keeps its 3D length; saving drops y as `addAdditionalSaveData` does. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Matches `StructureTemplate.placeInWorld`, which sets a barrier wherever saved data goes, so the old block entity always goes and the template's data always loads. CI parity is green at this head. Queued.
🤖 Agent: Ludovic's AI agent (Claude), triage. Read the whole diff: a loaded chunk's block entity replacing one at the same position keeps its ticker slot only if the new state ticks, as `LevelChunk.setBlockEntity` + `updateBlockEntityTicker` do. CI parity is green at this head. Queued.
The server's copy of a player never ran LivingEntity.refreshDirtyAttributes,
so after /attribute ... minecraft:scale it kept the old 0.6 x 1.8 box until
the pose changed: a small player was sent back from every wall they walked
along and suffocated next to one. guest_server_tick now runs aiStep's spin
countdown, refreshDirtyAttributes (onAttributeUpdated(SCALE) ->
refreshDimensions) and updatePlayerPose in vanilla's order, and in_wall
takes the eye box from the body's width (Entity.isInWall, width * 0.8)
instead of a fixed 0.48.
Part of #3684.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Redesign R2 (the animated night sky behind every page), stacked on #3636 which merged; approved by Ludovic 2026-10-29 (8 px/s clouds accepted); launcher fmt/clippy/tests and sky.rs tests pass on this head, CI green.
ClientLevel.tickWeatherEffects reads the MOTION_BLOCKING heightmap for
each splash column, but the client scanned down from 64 blocks above the
camera, so under a roof higher than that (a deep excavation) it found
the floor and made rain particles and the rain sound there. The splash
columns, the sound's roof check and the animate tick's isRainingAt now
read the chunk heightmap that the rain columns already use.
Fixes#3810
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Redesign R1b (the night look's specimen page, pixel-cut plates in plate.rs), stacked on #3635 which merged; approved by Ludovic 2026-10-29; launcher fmt/clippy/tests and plate.rs tests pass on this head, CI green.
🤖 Agent: Redesign R1 (night tokens, Bricolage with Geist's Cyrillic, pixel-cut corners), approved by Ludovic on 2026-10-29; launcher fmt/clippy/278 tests and horizon-theme tests pass on this head, CI green.
LivingEntity.setPosToBed puts a sleeper at the bed's x/z centre and at
pos.y + getSleepHeight + 0.125, getSleepHeight being the top of the bed's
shape (0.5625). Horizon left out the 0.125, so the player and sleeping
villagers lay 0.125 lower, and the first-person camera (eye 0.2 + 0.15)
ended up inside the player's own head.
Fixes#4006
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.isInvisible reads shared flag 5, which a server sets with the
Invisibility effect or alone through Player.setInvisible, as pose plugins
(GSit's lay, crawl, spin) do to hide the real body under the pose. The client
read the flag for other entities only and hid its own body, cape, first-person
arm and name tag (and thinned its effect swirls) only for the effect, so the
real body showed under the pose.
Keep the own flag from SetEntityData (reset on login and on a respawn without
KEEP_ENTITY_DATA) and use it with the effect wherever the own player's
isInvisible is asked.
Fixes#3788
Co-Authored-By: Claude Opus 5.5 <[email protected]>
shell.rs draws the frame's parts: the sidebar's translucent plate from the window's top to its
bottom (wordmark, instances, servers, links, version), rows and tabs on pixel-cut plates when
selected and eased in at half opacity when hovered, the account chip with the signed-in
account's face. The top bar sits over the pages only, without a plate. Java instances say
their version and loader in the sidebar and get no preparing state. hover_layer's easing is
now hover_fade, for layers of any shape; blocks::side_item is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The one intended difference from vanilla's server (MinecraftServer.getServerModName):
the harness checks Horizon sends brand=horizon, then compares the rest as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.itemAttackInteraction ran only for a mob victim, so smashing a
player never stopped the fall, never fired the Wind Burst explosion on the
attacker and never wore the weapon.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
In vanilla Minecraft (26.3), items equipped in the head slot without an
armour asset (such as carved pumpkins, player heads, and mob skulls) are
rendered via CustomHeadLayer with ItemDisplayContext.HEAD transform
(down 0.25, rotated 180 degrees, scaled 0.625; skulls scaled 1.1875) for
both the local player, remote players/guests, and humanoid mobs.
Additionally, equipment assets can specify multiple layers under their
layer kinds (e.g. equipment/leather.json has layer 0 leather and layer 1
leather_overlay). Horizon previously only parsed the first layer index 0,
causing overlays like leather_overlay to be dropped.
- ArmorStandAssets: load all humanoid and wings layers in equipment JSON
definitions into a list of layers.
- In armor_layers and armor_stand_geometry: render all humanoid layers
for equipped items in order, including non-dyeable overlays.
- custom_head_geometry: implement CustomHeadLayer for worn non-armour
head items and skulls, drawing worn skulls with their special model
(including player profile skin texture) and items (e.g. carved pumpkin)
via the item rendering pipeline.
- Hook custom_head_geometry into player model rendering, inventory preview,
remote players/mobs, and LAN guests.
- Update port ledger mappings for CustomHeadLayer and EquipmentLayerRenderer.
Co-Authored-By: Antigravity <[email protected]>
The item billboard of eggs, snowballs, ender pearls, fireballs and llama
spit took its right axis from forward x Y. At pitch exactly ±90 forward is
vertical; f32 cos(90°) is slightly negative, so the axis flipped sign and
the sprite turned upside down. It also ignored third-person front view.
ThrownItemRenderer.submit rotates the item by CameraRenderState.orientation
(Camera.setRotation: rotationYXZ(π - yaw, -pitch, 0), turned round in front
view). Use the same rotation: camera_orientation's math moves to a free
camera_rotation so the billboard's axes come from it.
Part of #3393
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Opening a world fullscreen could crash ~8s into the loading screen (#3866).
Two faults stacked up:
- draw_frame answered CurrentSurfaceTexture::Validation with Err on every
frame; the loading screen runs ControlFlow::Poll, so the warn ran ~16k
times a second (130k log lines in the report) while nothing recovered.
- SpriteAnimator::new asked the animated sprites pipeline for its bind
group layout after the error scopes in pipeline() had been popped. A
device lost during that window makes create_compute_pipeline return a
silent invalid placeholder (device-lost errors bypass the scopes), so
the scopes saw nothing, and the out-of-scope get_bind_group_layout then
hit wgpu's uncaptured-error path and panicked. pipeline() now returns
the layout queried while the scopes are open, where a validation error
is caught and the CPU path takes over, as it already does for drivers
without working compute shaders.
Validation on acquire now gets the swapchain a fresh configuration like
Outdated does, then reattaches the window like Lost does once that has
not helped, and a frame is skipped (Ok(false)) instead of failed; the
counter resets on the next acquired frame.
Fixes#3866
Co-Authored-By: Claude <[email protected]>
🤖 Agent: pr-triage-1: read the new head. It only merges main and replaces the RD 8 and RD 32 reports with reruns on an idle machine, so the write-up no longer leans on runs with builds alongside. tests/test_perf_bench.py and the public tree check pass. skip-release stays. Thank you!
The scrollers on the stonecutter and merchant screens only moved with the
mouse wheel: pressing and dragging them did nothing. Port
StonecutterScreen.mouseClicked/mouseDragged and
MerchantScreen.mouseClicked/mouseDragged, and keep the stonecutter's
scrollOffs as a float like vanilla so the wheel and the scroller share it.
Part of #3518.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
The wither checks (WitherSkullBlock.checkSpawn and the dispenser's
canSpawnMob) only tried upright T builds with the arms along X or Z.
Vanilla's BlockPattern.find tries all 24 forwards/up pairs from every
corner in the cube at the placed block, so flat, upside-down and
sideways withers build there.
Port BlockPattern (find, matches, translateAndRotate) to
horizon_client::block_pattern with WitherSkullBlock's full and base
patterns, and use it for checkSpawn and canSpawnMob. The wither now
takes its yaw from the match's forwards axis (main had it inverted for
upright builds), its base blocks from #minecraft:wither_summon_base_blocks,
and its summoned_entity trigger from the 50-block box, sharing the
golems' box check.
Fixes#3098.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of #3996: while native_block_entity_tick runs, the block entity
at its position is an empty-id placeholder; a save then skips it rather
than writing `id: ""`. The hook's doc says so, and that clients (the
window's own in single player too) don't get mod block entities yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths; tipped/spectral arrows and text/block/item displays drawn alike by host and LAN guest, save and reload identical, the ominous item spawner seen by a LAN guest. Mojang rules, supply chain, per-tick cost read.
FallingBlockEntity.tick runs Entity.applyEffectsFromBlocks after its move,
and BubbleColumnBlock.entityInside lifts or drags it like any other entity
(Entity.handleOnAboveBubbleColumn / handleOnInsideBubbleColumn). Horizon's
fall_step only looked for cobwebs on the path, so sand dropped into a soul
sand column sank to the bottom.
The cobweb sweep now visits each block checkInsideBlocks reaches once, with
vanilla's intersects flag (every block of a move a block or longer, else
only those the box ends in), and a bubble column there pushes the block and
resets its fall below the surface. The bubble column lookup and push are
split out of bubble_column_push so items and mobs share them. Velocity is
zeroed on collided axes before the effects, as Entity.move does.
Part of #3393
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dedicated server draws nothing, yet it built the full HUD atlas (GUI,
particles, maps, skins) and the title panorama. The integrated server's
game already holds only the font, which books and chat measure with, and a
one-pixel panorama; both now come from UiResources::server, and the
dedicated server's synchronous load uses it too.
Idle, fresh world, 3 interleaved runs: RSS 836/836/841 -> 771/792/765 MB,
ready 5.05 -> 4.32 s (median).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): 6da84cf19 was proven on union/pt3-w2; this head only merges main and adapts main's two new held-item tests to the Tag custom name. Workspace check, 1508 tests and the 11 item/save harnesses pass on it.
A block entity whose id is a mod's loads as BlockEntity::Mod (its id and
saved compound) instead of staying raw chunk data, and saves back as it
was. Three hook points let a native mod give its blocks block entities
(native_block_entity), say which tick (native_block_entity_ticks) and
tick them on the server in vanilla's order (native_block_entity_tick).
Player placement, /setblock, dispensers and horizon-ctl make them through
one Game::new_block_entity.
Fixes#3966.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 128 atlas skin slots went round every frame once more skins were in
use than slots: update_remote_skins touched every player-list entry's skin,
each frame's uploads pushed out the least recently shown one (even one drawn
the frame before), and that one came back the next frame by pushing out
another. Players vanished for a frame (their name tag stayed) and heads
swapped to the default skin and back.
- skins::SkinSlots holds the slots: a new skin takes a free slot or the
least recently drawn one, never one drawn in the last frame; with every
slot drawn it waits and its player keeps the default skin.
- Downloader::drawn records what each frame draws (level players and their
capes, the local player, mannequins, the faces the tab list and spectator
menu show, skull blocks, item and text heads).
- Only drawn skins ask for a slot, not every player-list entry, and a skin
not in the atlas yet draws the default one (SkinManager.createLookup's
getNow(DefaultPlayerSkin.get)) instead of a missing texture.
Fixes#3829
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Generated worlds saved impossible block light at chunk borders: an air
block at 0 beside one at 6 (#2786). A column is lit from the blocks of
its whole 3x3, so it holds light from a neighbour whose own light job
has not landed yet. An edit in it then runs the incremental relight,
which reads only lit columns: its darkening removes that neighbour's
light and nothing can put it back, and the neighbour's job landing
does not relight the column. The stale border stays, and is saved.
Columns whose light an edit (or a replayed edit) changed beside a
loaded, unlit column now get a full light job, as vanilla's light
engine, with one light storage for every chunk, never loses it.
Fixes#2786
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by Ludovic's AI agent (pr-triage-2, Claude) at c566e69c6, merged with current main: the End gateway follows EndGatewayBlock.entityInside and Entity.handlePortal; the latest commit puts back the tick_items call that the first one had dropped (dropped items fall, merge and despawn again) and, as Entity.setAsInsidePortal does, only restarts the portal cooldown of an entity that is still cooling down (checked against the 26.3 bytecode). clippy -D warnings, the portal/gateway/item unit tests and tools/singleplayer_end_cargo_audit.py on both paths pass (cow and a 7-diamond stack arrive at [1035.5, 76.0, 1.5]). Thanks @levzzz5154.
Minecraft.tick picks the crosshair's hitResult once, before
handleKeybinds, and startAttack, startUseItem, continueAttack and
pickBlockOrEntity all read that one result. Horizon re-picked for every
click, so after a creative START predicted the break, the use in the
same tick aimed past the broken block (at nothing, or the block behind).
InteractionInput::tick now pins the tick's hit result around the clicks.
The server also deferred a creative START to its guest tick, after the
UseItemOn that followed it in the same batch, which then placed against
the old block before it broke. ServerPlayerGameMode.handleBlockBreakAction
destroys at once for an instabuild player (destroyAndAck "creative
destroy"); the host now does too.
Fixes#3006
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by Ludovic's AI agent (Claude): the review point is addressed (decorated protos still save as minecraft:features); the forced ticket rings follow FORCED_TICKET_LEVEL 31 rising one per ring; merged with main, clippy -D warnings and the horizon-client tests (forced_chunks included) pass. Queued; the merger waits for CI parity.
🤖 Agent: pr-triage-2 checked the pin: 3f99a5c60 is horizon-rust/horizon-mixins main, 8 commits ahead of 0d6b103c7 (preserved standard macros, Windows verbatim output paths), nothing behind; only MIXINS_REV changes and explicit per-bundle pins still win. Thanks @levzzz5154.
plate.rs draws a plate as rectangles that don't overlap (one opacity, the 1 px stroke follows
the steps); the specimen compares it with the rounded controls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FishingHook.tick runs on a client, easing towards the last update while it
flies, floats and rides what it hooked. Horizon only eased towards the
updates, which come every five ticks, so the bobber stood still for two
ticks of five and jumped.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Plates cut their corners in two 4 px steps (PIXEL_CORNER); RADIUS_PLATE is gone
from the night tokens. tools/bake_bricolage.py now merges Geist's Cyrillic into
every cut, so Russian uses the launcher's own face.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c32dddb1f: carried and worn blocks draw their block-state model with vanilla's per-face default tints (block_tint harness against 26.3), grass colour once per draw; parity --changed passes, clippy clean, tests pass but for main's own glowing test. Merging.
🤖 Agent: verified by server-tick-1: proto light views built once per proto state (streaming span 10-24 → 3-4 ms/tick on 25 explore bots, RSS unchanged); new generation_saves test fails on main and passes here; pr_ready --quick green; merging once CI parity passes.
🤖 Agent: pr-triage-1 merged main once more (fast-forward): your Polish checks sit beside main's new test that every key has a Russian value. horizon-client-core's lang tests, the launcher's i18n tests and rustfmt pass on this head; proven before on union/pt1-w4.
Uploads staged every write through wgpu's StagingBelt, which gives a write
larger than its chunk a buffer of exactly that size, keeps it for good and
only reuses it for writes that fit. The entity batch outgrows the 1 MiB chunk
with a few hundred mobs and grows a little each tick while more appear, so
every tick took a new buffer: about 1 GB more per hundred mobs past ~750,
never given back, until the game was killed (#2826).
Writes larger than the chunk now take a power-of-two slice and copy only
their bytes, so the buffers the belt keeps are reused: one per doubling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The darkness effect's blend (MobEffectInstance.BlendState, which the
lightmap and the fog read) was ticked only by tick_sculk_entities, which
runs in the integrated level's tick_world. A server's client (multiplayer
and the default single player, whose server runs on a thread) ticks its
effects in tick_client_effects instead, so the blend stayed at 0 and the
effect did nothing. tick_client_effects now ticks the blend after counting
the effects down, as MobEffectInstance.tickClient does.
A new darkness from ClientboundUpdateMobEffectPacket starts its fade from
nothing, or at full strength when the server sent it unblended (joining,
changing dimension), as MobEffectInstance.skipBlending does.
Part of #3393.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two tests main added (entity_nbt, mobs) set custom_name to a string and compared custom_name() with Option<&str>; with this PR it is the component tag and the plain text is an owned String.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CushionItem.useOn returns SUCCESS, so the hand swings: place_cushion now
swings, and a client of a server predicts the same SUCCESS once
Cushion.canBePlacedAt holds (the placement checks are split out as
cushion_spawn for both). Cushion.interact sits the player down through
Entity.startRiding, which first stops riding the current vehicle, so a
player on one cushion moves to another instead of being refused.
LivingEntity.stopRiding off a cushion now plays the get-up sound and
puts the rider beside it (shared with getting up on sneak).
Fixes#3117
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pack screen, F3+T and options-set staged the whole reload (overlay
root, models, atlas, pack tables, mip chains) on the render thread, so a
large pack froze the window for seconds. They now run it on a worker under
the progress overlay, as server packs already did (Minecraft.
reloadResourcePacks under a LoadingOverlay); a reload asked for while one
runs waits for it (pendingReload).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths, and the throws levzzz asked for (walking/jumping carry the movement, none after a tick without a move) for host and guest on both paths. Mojang rules, supply chain, per-tick cost (a Vec3 copy per guest turn) read.
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1. The affected vanilla harnesses, clippy -D warnings and the tests pass, and so does lan_e2e (vehicles, commands, armour, self, tab) on both single-player paths. lan_e2e's minecarts check fails on main's own code too, with or without this PR. Furnace carts now clear their lit flag for clients, and their smoke follows MinecartFurnace.tick. Thank you!
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): launcher-only change; on this exact head launcher fmt, clippy -D warnings and 299 tests pass on the Mac mini, clean merge with main. Asset refill only through download_for_owner (owners only).
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1: the affected vanilla harnesses pass (the only failures are main's own or another PR's), clippy -D warnings and the tests pass, the launcher's checks pass, and lan_e2e passes vehicles, commands, armour, self and tab on both single-player paths. Thank you!
Breeding spawns the baby through spawning::finalize, which now gives a fox a mouth item and a goat a lost horn and a random screaming roll; Fox.getBreedOffspring and Goat.getBreedOffspring run no finalizeSpawn (Fox.java, Goat.java). The baby fox is emptied, the baby goat keeps both horns and gets a parent's screaming or one chance in fifty.
T and / always opened the chat with the caller's text. ChatComponent.createScreen (ChatComponent.java:442) restores the saved draft, and ChatScreen.removed (ChatScreen.java) keeps it on Escape when Save Chat Drafts is on.
A dedicated server never kicked a spamming guest: chat-spam-threshold-seconds and command-spam-threshold-seconds were read but unused. Vanilla adds 20 per message or command, decays one a tick and disconnects over the threshold with disconnect.spam, except for operators (ServerGamePacketListenerImpl.detectRateSpam, TickThrottler.java:23).
A save's resourcepacks/resources.zip was never loaded, so its textures were missing. Vanilla WorldOpenFlows.loadBundledResourcePack pushes it as a local pack (ServerPackManager.pushLocalPack, ServerPackManager.java:61) before the world connects.
Player and disguised chat showed at once, so Chat Delay was ignored. ChatListener.handleMessage, tick and flushQueue (ChatListener.java:75-120) queue the lines and show one per delay.
The wheel over an option button only scrolled the list. CycleButton.mouseScrolled (CycleButton.java:115) cycles its value, up going back, and the list does not move.
Entity event 55 was ignored, so another player's main and off hand kept their items after a swap on the server; vanilla LivingEntity.handleEntityEvent calls swapHandItems (LivingEntity.java).
Only structure mobs ran Fox.populateDefaultEquipmentSlots, so a fox from a natural or spawner spawn never held an emerald, egg, rabbit foot or the like (Fox.java).
Only structure mobs and the probe ran Goat.finalizeSpawn, so a goat from a natural or spawner spawn never screamed, lost a horn or had its ram cooldowns (Goat.java, GoatAi.initMemories).
HurtByTargetGoal.alertOthers called every nearby wolf, including other owners' and the attacker's own tamed pets; vanilla checks TamableAnimal.getOwner and considersEntityAsAlly (HurtByTargetGoal.java, TamableAnimal.java).
A tamed skeleton horse obeyed its rider without a saddle; AbstractHorse.getControllingPassenger (AbstractHorse.java) needs the saddle, and SkeletonTrapGoal only tames the trap horse.
A splashed water bottle did nothing to axolotls; AbstractThrownPotion.affectEntitiesAround (AbstractThrownPotion.java) rehydrates each axolotl in the box, adding 1800 air up to the maximum.
A landing horse played the generic fall sound and no thud; AbstractHorse.causeFallDamage (AbstractHorse.java) plays entity.horse.land (baby variant for babies) at 0.4 for falls over one block.
A bee in the air never gave off its flap vibration, so sculk sensors never heard it; Entity.processFlappingMovement with Bee.isFlapping (Bee.java) emits minecraft:flap.
Axolotl.getBreedOffspring took the first parent's colour, never blue and never persistent; vanilla rolls one in 1200 for blue, else picks either parent and sets the baby persistent (Axolotl.java).
A saved mannequin lost its profile, so after a reload it wore the default skin. Vanilla saves and reads it as ResolvableProfile (Mannequin.addAdditionalSaveData / readAdditionalSaveData, Mannequin.java:126 and :144).
A blast never reached item frames, so they stayed up and kept their item. Vanilla kills a non-fixed frame in reach and drops it with its item (BlockAttachedEntity.hurtServer, ServerExplosion.hurtEntities; ItemFrame.java:173).
A guest who toggled the hat skin layer was not announced: vanilla sends UPDATE_HAT to all players in handleClientInformation (ServerGamePacketListenerImpl.java:2180), and the join entry now carries the hat state too.
Ravagers only broke leaves, so crops in their path survived; CropBlock.entityInside (CropBlock.java:153) destroys a crop a ravager's box enters when mobGriefing is on.
Picking up an item the player threw never granted thrown_item_picked_up_by_player, so You've Got a Friend in Me was unreachable; ServerPlayer.onItemPickup (ServerPlayer.java:2324) fires it with the stack left after the take.
A piglin taking gold you threw never granted thrown_item_picked_up_by_entity, so Oh Shiny stayed locked; LivingEntity.onItemPickup (LivingEntity.java:3429) fires it for the taker, before the stack is split.
Walking ignored live shulkers, so the player passed through them; Shulker.canBeCollidedWith (Shulker.java:487) returns isAlive and Entity.canCollideWith uses it.
The integrated host ignored entityDistanceScaling, so guests saw entities at the default range; IntegratedServer.getScaledTrackingDistance (IntegratedServer.java:451) scales each tracking range by it.
A guest in spectator mode sent no invisible flag, so others saw a full model; ServerPlayer.updateInvisibilityStatus (ServerPlayer.java:2054) sets it for every spectator.
A straw bed stayed in place after its sleeper woke up, but the Overworld's straw_bed_rule has destroy_on_leave, so vanilla breaks it (AbstractBedBlock.java:119 onStopSleeping, StrawBedBlock.java destroyBed).
MinecraftServer.enforceGameTypeForPlayers sets every player to the server mode when it is forced; guests already online kept their own mode and only got the default at join (MinecraftServer.java:1412).
MinecraftServer.setDifficulty sends ChangeDifficulty to every player; /difficulty changed only the host, so guests kept the old difficulty until they rejoined (MinecraftServer.java:1307).
A mooshroom calf took its first parent's colour every time; MushroomCow.getOffspringVariant gives either parent's colour, and the other one 1 in 1024 times when both match (MushroomCow.java:275).
MushroomCow.addAdditionalSaveData and readAdditionalSaveData store the colour as Type; only foxes wrote it, so a brown mooshroom reloaded red (MushroomCow.java:214, :221).
EquipmentLayerRenderer.renderLayers draws every layer of an equipment asset; only layer 0 was loaded, so leather armour lost its overlay layer (leather.json, EquipmentLayerRenderer.java:51).
The window and LAN guests are clients of the server thread, so the arrow
effects added for Arrow.tick, Arrow.handleEntityEvent and ArrowModel.setupAnim
never reached them: the server's arrow drew its particles into a world nobody
sees, and a client's copy of an arrow never held a shake timer.
The server no longer draws or randomises Arrow.makeParticle (it is the
client's `Level.isClientSide` branch). It sends Arrow.ID_EFFECT_COLOR with a
tipped arrow's entity data and entity event 0 when the arrow has been stuck
for 600 ticks. A client ticks the particles itself (two in flight, one every
fifth tick in a block, the client's own random) and bursts twenty on event 0.
A client's arrow now starts its shake when it lands (AbstractArrow
.onSyncedDataUpdated) and runs it down each tick, so the roll shows.
A tipped arrow left stuck for 600 ticks kept its potion and never burst. Vanilla Arrow.tick sends entity event 0 (20 particles in the potion colour) and sets a plain arrow as the pickup item.
Tipped arrows left no entity-effect particles in flight or stuck in a block. Vanilla Arrow.tick and Arrow.makeParticle draw two per tick in flight and one every fifth tick in the ground (Arrow.java).
Only chests and banners showed the breaking overlay. Vanilla SkullBlockRenderer, ShulkerBoxRenderer, BellRenderer, LecternRenderer and CopperGolemStatueBlockRenderer submit their models for crumbling too.
Taking an item an entity threw never fired thrown_item_picked_up_by_player and allay deliveries had no thrower. Vanilla ServerPlayer.onItemPickup and BehaviorUtils.throwItem (PickedUpItemTrigger) do both.
A stuck arrow kept a fixed model pose; vanilla ArrowModel.setupAnim (ArrowRenderer) rolls the root by -sin(shake*3)*shake degrees while shakeTime lasts.
With forgive_dead_players on, wolves, bees, polar bears and other neutral mobs kept their anger after the player died. ServerPlayer.tellNeutralMobsThatIDied (ServerPlayer.java:991) makes mobs within 32 by 10 blocks run NeutralMob.playerDied, which stops the anger at that player (NeutralMob.java:122).
A bred baby goat started without ram or long jump cooldowns and was never screaming. Goat.getBreedOffspring (Goat.java:152) runs GoatAi.initMemories, then takes screaming from a random parent or rolls 2 percent.
Turtles used the generic fluid travel in water (0.02 speed, 0.8 drag). Turtle.travelInWater (Turtle.java:266) moves at 0.1 with 0.9 drag and sinks slowly unless it has a target or is going home near its home.
Mobs stuck in powder snow never jumped out: the goal was missing from their registrations. Vanilla ClimbOnTopOfPowderSnowGoal jumps when the block above is powder snow or open (ClimbOnTopOfPowderSnowGoal.java:23).
A projectile striking an amethyst block played nothing; vanilla plays block.amethyst_block.chime at the block with a random pitch (AmethystBlock.java:18).
A sheared bogged kept its mushrooms on the head. BoggedModel.setupAnim (BoggedModel.java:62) sets the mushrooms part visible only while the bogged is not sheared.
Clicking styled text in a dialog message or item description did nothing. DialogBodyHandlers.java:54 and :108 run the text's click event through DialogScreen.runAction.
A saddled horse with no rider drew its left and right saddle lines. EquineSaddleModel.setupAnim (EquineSaddleModel.java:64) shows the riding parts only while the horse is ridden.
The downgrade check compared Data.DataVersion; vanilla LevelVersion.parse takes Version.Id (no Version reads as 0, no Id as the current one) and names a Version without a Name as the current version. Both world checks now share that reading, and the play doc lists the new outcomes.
The dispenser paths for spawn eggs and armor stands skipped the stack's custom name that the hand-placed paths now apply. Entity.applyImplicitComponents (Entity.java:4281) names the entity on every spawn from an item.
Horizon opened a newer world without asking. WorldOpenFlows.openWorldCheckVersionCompatibility (WorldOpenFlows.java:352) asks for a backup first, with the red downgrade question (BackupStatus.DOWNGRADE in LevelSummary.java).
A renamed armor stand item placed an unnamed stand. EntityType.createDefaultStackConfig (EntityType.java:171) applies the stack's components, custom name included, through Entity.applyImplicitComponents (Entity.java:4281).
Horizon opened a world from another version series without a check. WorldOpenFlows.openWorldCheckVersionCompatibility (WorldOpenFlows.java:352) shows the red Incompatible version AlertScreen and keeps the world closed.
Piglins took the nearest wanted item within 9 blocks even behind a wall. NearestItemSensor.doTick (NearestItemSensor.java:24) takes the nearest visible one within 32, then GoToWantedItem walks to it within 9 (PiglinAi).
Mobs fell through honey blocks; only the player slid. HoneyBlock.entityInside (HoneyBlock.java:75) slows any living entity beside the block, with the slide sound and the entity event 53 particles.
An allay walked to a matching item behind a wall. NearestItemSensor.doTick (NearestItemSensor.java:24) keeps only items the mob has a line of sight to.
The world list showed a world open in another instance as an ordinary world. DirectoryLock.isLocked (DirectoryLock.java:66) and LevelSummary.createInfo (LevelSummary.java:154) show it as Locked in red and it cannot be joined.
Pick block on a piston head gave nothing, since the head has no item form. PistonHeadBlock.getCloneItemStack (PistonHeadBlock.java:116) gives a sticky or plain piston by the head's type.
A spawn egg renamed in an anvil spawned an unnamed mob; the stack's custom name was never applied. Entity.applyImplicitComponents (Entity.java:4281) sets the custom name from the item on spawn.
Selectors parsed the rotation ranges but never filtered on them, so @e[y_rotation=...] matched regardless of facing. EntitySelectorParser.createRotationPredicate (EntitySelectorParser.java:188) checks the wrapped pitch and yaw.
Allay items were placed with the humanoid hand offset; AllayModel.translateToHand (AllayModel.java:120) uses the body, the right arm's pitch, a 0.7 scale and a shift.
A double click on a book page only moved the caret, so no word was selected. Vanilla MultiLineEditBox.onClick and MultilineTextField.selectWordAtCursor (MultiLineEditBox.java:89, MultilineTextField.java:184) select the word before the cursor.
The wheel only scrolled the list there, and Ctrl was ignored. VideoSettingsScreen.mouseScrolled (File.java:263) steps guiScale by one and resets the list to the top instead.
Enter and Space on a selected world did nothing, so keyboard players could not join from the list; vanilla WorldListEntry.keyPressed joins on selection keys (WorldSelectionList.java:602).
Enter did nothing on Direct Connection; vanilla DirectJoinServerScreen.keyPressed selects the server when the Join button is active (DirectJoinServerScreen.java:32).
Enter only confirmed a world rename, so it did nothing on Create New World; vanilla CreateWorldScreen.keyPressed calls onCreate on confirmation (CreateWorldScreen.java:360).
Hovering a ghost ingredient or result in a crafting grid or furnace showed nothing; vanilla GhostSlots.extractTooltip shows the ghost item for the hovered slot (GhostSlots.java:65).
Hovering an item icon in the Items tab showed nothing; vanilla ItemRowWidget.extractTooltip draws the item tooltip at the slot's corner (StatsScreen.java:512).
Dolphins ignored guardians and took their hits as attacks; Dolphin.registerGoals (Dolphin.java:158) adds AvoidEntity for Guardian and HurtByTargetGoal ignoring Guardian.
A dried ghast filled with water made no place-in-water sound. Vanilla DriedGhastBlock.placeLiquid (DriedGhastBlock.java:174) plays DRIED_GHAST_PLACE_IN_WATER after scheduling the fluid tick.
A boat passing over a lily pad left it in place. Vanilla LilyPadBlock.entityInside (LilyPadBlock.java:27) destroys the pad with its drops for an AbstractBoat on a server level.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at d8e912738 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a717d6d58 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1ffe68635 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
CarvedPumpkinBlock.trySpawnGolem uses BlockPattern.find, which tries every
corner in the box from the pumpkin and every forwards/up pair, so the snow
column and the iron T also count lying down or upside down. Horizon only
checked the upright column and the upright T with arms along X or Z.
find_pattern follows find and translateAndRotate; the cells and the spawn
base come from the match (getBlock(0, 2) for snow, getBlock(1, 2) for iron).
Fixes#3929
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In single player the server side skips the wick smoke, so the client draws it itself, as CandleBlock.useItemOn does on the client (AbstractCandleBlock.extinguish).
The block item in a copper golem's antenna slot was never drawn. BlockDecorationLayer.submit (BlockDecorationLayer.java:33) draws it at CopperGolemModel.applyBlockOnAntennaTransform (CopperGolemModel.java:311).
A note block with a player head on top always clicked, ignoring the head's note sound. NoteBlock.getCustomSoundId (NoteBlock.java:167) takes the sound from the skull above; no skull or sound means silence.
A drinking witch's potion was drawn in the hand with no potion contents. WitchItemLayer.applyTranslation (WitchItemLayer.java:17) holds the potion at the nose as a potion item.
An empty hand on a lit candle did nothing; only arrows lit them. CandleBlock.useItemOn (CandleBlock.java:75) calls AbstractCandleBlock.extinguish: unlit, wick smoke, candle extinguish sound, block_change.
item replace/fill/override accepted counts up to 99 above an item's max stack. ItemInput.createItemStack (ItemInput.java:21) throws arguments.item.overstacked instead.
Hovering a show_entity chat part showed nothing. With advancedItemTooltips on, vanilla lists the name, the entity type and the UUID (GuiGraphicsExtractor.componentHoverEffect; HoverEvent.java:84).
A spyglass in use was drawn in the hand, not at the eye. PlayerItemInHandLayer.renderItemHeldToEye (PlayerItemInHandLayer.java:40) clamps the head pitch to -30 to 90 degrees and holds the item on the head.
Zombie, skeleton, creeper and player heads in a mob's head slot were not drawn. CustomHeadLayer.submit (CustomHeadLayer.java:44) puts the skull model on the head, and resolveSkullRenderType (CustomHeadLayer.java:69) takes a player head's skin from its profile.
A brush in use was held still. FirstPersonHandsAndItemsRenderer.applyBrushTransform (FirstPersonHandsAndItemsRenderer.java:286) swings it back and forth over the use.
Hovering a chat part with an insertion never asked for the pointing hand; vanilla requests it in ChatComponent.java:672 when Shift is held (ChatScreen passes insertionClickMode).
A stand broken by a creative hand or the second hit made no plank particles; ArmorStand.showBreakingParticles sends ten oak plank block particles from the box (ArmorStand.java:369).
Spell particles stayed visible in the spyglass view; SpellParticle.isCloseToScopingPlayer sets their alpha to 0 within 3 blocks of the eye in first person while isScoping (SpellParticle.java).
A sheep named jeb_ kept its dyed colour; SheepRenderState.getWoolColor fades it through ColorLerper.getLerpedColor(SHEEP) (SheepRenderer.java, MobRenderer.checkMagicName).
Switching a lever on showed no dust puff at the lever. LeverBlock.useWithoutItem (LeverBlock.java:56) calls makeParticle at full scale on the clicking client when the lever ends up powered.
Hitting, stepping on or right-clicking a redstone ore lit it with no particles. RedStoneOreBlock.interact (RedStoneOreBlock.java:61) calls spawnParticles each time, lit or not.
Pick-block on a light gave a level 0 light. LightBlock.getCloneItemStack and setLightOnStack (LightBlock.java:100,104) put the level into the item's block state.
Placing a dried ghast was silent. DriedGhastBlock.setPlacedBy (DriedGhastBlock.java:189) plays block.dried_ghast.place, or place_in_water when waterlogged.
Mining a stack of 2 to 4 turtle eggs removed the whole stack; TurtleEggBlock.playerDestroy (TurtleEggBlock.java:138, decreaseEggs) plays the break and keeps the rest.
Right-clicking the side of a wall hanging sign with a hanging sign opened the sign instead of chaining. WallHangingSignBlock.useItemOn passes the use on when the hit is not on the editable faces (WallHangingSignBlock.java:53).
A thrown trident faster than 0.6 that stuck in pointed dripstone left it standing. SpeleothemBlock.onProjectileHit destroys it, reading the deltaMovement from before the hit (SpeleothemBlock.java:181, AbstractArrow.onHitBlock).
Only the local player got the connection particles from a vault, so guests connected to it saw none. VaultBlockEntity.Client.emitConnectionParticlesForNearbyPlayers loops over every connected player (VaultBlockEntity.java:172).
Middle-click on a statue gave it standing, whatever its pose. CopperGolemStatueBlock.getCloneItemStack sets the block_state component to the pose (CopperGolemStatueBlockEntity.getItem, CopperGolemStatueBlock.java:134).
Middle-click on a decorated pot gave an empty pot. DecoratedPotBlock.getCloneItemStack returns createDecoratedPotInstance(decorations), so the sherds come with the pick; the pot's item only comes with include_data (DecoratedPotBlock.java:211).
A wind charge burst that reached a bell sent a use on its top face, which the bell rejected, so it stayed silent. BellBlock.onExplosionHit calls attemptToRing with no direction, so the bell rings on its facing (BellBlock.java:197).
Middle-click on a flower pot with a plant gave an empty pot. FlowerPotBlock.getCloneItemStack returns new ItemStack(potted) when the pot is not empty (FlowerPotBlock.java:115).
A non-uprooted creaking heart with pale oak logs on six sides never made its idle creak; CreakingHeartBlock.animateTick (CreakingHeartBlock.java:65) plays it 1 time in 16 while CREAKING_ACTIVE is on.
A right-click with an empty hand on the lit candle of a candle cake did nothing. CandleCakeBlock.useItemOn extinguishes it when the hit is above half a block, with the extinguish sound and a block change event.
The rocket of a gliding player started at the player's feet; vanilla offsets it 0.5 block toward the hand holding it (Entity.java:2688 getHandHoldingItemAngle, FireworkRocketEntity.java:138).
Fox.addAdditionalSaveData writes Sleeping, Sitting and Crouching from its flags and Fox.readAdditionalSaveData reads them back; a reload no longer wakes a sleeping fox (Fox.java:445). Trusted players are still not ported.
AbstractNautilus.checkRestriction sets the home at the nautilus with getNautilusRestrictionRadius (32, or 16 for a baby or saddled one) when it is not leashed or ridden (AbstractNautilus.java:237, :241).
Sheep.handleEntityEvent(10) sets eatAnimationTick to 40 and EatBlockGoal.start broadcasts event 10 (Sheep.java:119, EatBlockGoal.java:40); the server now sends it and clients start and count down the eating head.
A baby took one parent's colour by a coin flip; DyeColor.getMixedColor mixes them with the two-dye crafting recipe, else picks one (Sheep.java:281, Wolf.java:608, Cat.java:383).
Turtles in water or babies played the land ambient; Turtle.getAmbientSound plays TURTLE_AMBIENT_LAND only on land and grown up, else Mob's silent default (Turtle.java:176).
A zoglin with an attack target grunted its ambient instead of entity.zoglin.angry; Zoglin.getAmbientSound picks ZOGLIN_ANGRY when ATTACK_TARGET is set (Zoglin.java:286).
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 217216e26 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 825f8f884 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c455bb6ec in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a11e54e7b in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c7aca8489 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
LivingEntityRenderer.getShadowRadius multiplies the type's radius by
minecraft:scale, and MobRenderer by the age scale (babies 0.5, or the
species' own: goat 0.55, camel 0.6...). Villagers and striders halve a
baby's shadow again, turtles take 0.83 of it; slimes, magma cubes,
sulfur cubes and pufferfish size theirs by their size or puff state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zombies, husks and zombie villagers wandered with the plain stroll only. Vanilla Zombie.addBehaviourGoals adds MoveThroughVillageGoal(1.0, onlyAtNight, 4, canBreakDoors) at priority 6 (MoveThroughVillageGoal.java): within 6 sections of a village, pick the land spot (15, 7) whose nearest taken, unvisited village POI is closest, path to that POI (or ten blocks toward it, or only as far as a wooden door), and remember the 16 last POIs reached. The weighted land position now also honours a restricted home like vanilla's LandRandomPos.getPos.
Clicking a sign with a run_command in its text did nothing. SignBlock.useWithoutItem and useItemOn call SignBlockEntity.executeClickCommandsIfPresent (SignBlockEntity.java:182): each line of the side the player faces runs its run_command (as the player, from the block centre, permission level 2, no output: createCommandSourceStack with CommandSource.NULL), opens its show_dialog or logs its custom action, but only when the sign has allow_op_features; otherwise the action bar says click actions are disabled. A waxed sign still plays its failed-interaction sound, and a sign that ran one does not open the editor.
Golems strolled anywhere at 0.6 (the plain stroll) and never went home. Vanilla IronGolem.registerGoals (IronGolem.java:68) adds MoveBackToVillageGoal at priority 2 (a stroll every 10 ticks, toward the section within 2 closest to a village, only outside the village) and GolemRandomStrollInVillageGoal at 4 (every 240 ticks: 3 in 10 anywhere, else toward a villager who wants a golem or a taken POI in a village section, from the level's random). Both are RandomStrollGoal subclasses, so Stroll takes an overriding spot picker and canUse test; the village sections and POI tickets come from the villager brain's helpers.
A hanging sign used on another hanging sign always chained below it, even when the aimed sign had a run_command to run. SignBlockEntity.canExecuteClickCommands (SignBlockEntity.java:178) is a waxed sign whose facing side has one, and CeilingHangingSignBlock and WallHangingSignBlock shouldTryToChainAnotherHangingSign pass only without it. The game now asks the sign, and hands the answer to the wall hanging sign check through PlayerContext.
A piglin that charged with a spear stayed on its target. SpearRetreat starts on the RETREAT status with a target, a spear and no item in use when LandRandomPos.getPosAway finds a spot 9 to 11 blocks away (checkExtraStartConditions), remembers it as SPEAR_FLEEING_POSITION, walks there for up to 100 ticks while looking at the target, and ends by erasing the status so the next approach starts (SpearRetreat.java).
A charging piglin stood still. SpearAttack.tick moves it every tick: run at the target (SpearAttack.java), and within 2 blocks of it or at the end of the path pick a spot 6 to 7 blocks past it (LandRandomPos.getPosAway, 2 more blocks on a mount) to run to, erasing it at the end of the path so it runs at the target again, until SPEAR_ENGAGE_TIME ends.
After closing in, a piglin with a golden spear never lowered it to attack. SpearAttack starts on the CHARGING status with a target, a spear and no item in use: it becomes aggressive, sets SPEAR_ENGAGE_TIME to the weapon's use time (KineticWeapon.computeDamageUseDuration), erases SPEAR_CHARGE_POSITION and starts using the spear (SpearAttack.start); the charge counts down while it looks at the target and ends in the RETREAT status (SpearAttack.java).
A piglin holding a golden spear (about 1 in 20 adults) walked up and meleed. PiglinAi.initFightActivity puts SpearApproach before the melee behaviour: with a target, a spear and no item in use, and no SPEAR_STATUS yet (checkExtraStartConditions), it sets the status APPROACH and walks to the target until it is within 10 blocks (SpearApproach.java), then the status is CHARGING. A piglin with a spear no longer melees (MeleeAttack.isHoldingUsableNonMeleeWeapon).
A potion that hit a block had no face and never put anything out. Thrown potions now keep the block and the face they entered it by, and AbstractThrownPotion.onHitBlock (server side, before the entity effects) douses the cell in front of that face, the cell behind it and the four horizontal neighbours when the potion is in #douses_fire. AbstractThrownPotion.douseFire destroys a fire block without drops, puts out a lit candle or candle cake (AbstractCandleBlock.extinguish) and a lit campfire (level event 1009, the block-change game event, lit false) (AbstractThrownPotion.java:48, :122).
A water potion only hurt endermen, and nothing it splashed on stopped burning. AbstractThrownPotion.affectEntitiesAround goes over every living entity in the potion's box grown by (4, 2, 4) and under 4 blocks away: #hurts_water_sensitive_entities hurts the sensitive ones (blaze, snow golem, strider, enderman) with indirectMagic and #extinguishes_entities calls extinguishFire on the burning ones, players and armor stands included (AbstractThrownPotion.java:94).
Entity.extinguishFire plays the extinguish sound for a burning entity and then clears its fire; AbstractCandleBlock.extinguish unlights a candle or candle cake with its sound and the block-change game event (no source entity, and no smoke from a server). Mobs only lost their fire in water or rain, and candles only to a player's empty hand or a wind charge.
A thrown potion hurts through DamageSources.indirectMagic(potion, owner), which bypasses armor, is resisted by witches and wolf armor, and, unlike magic, knocks back from the potion. Mobs only had magic (no knockback) or a player attack. Add the kind, its constructor and the damage_type id, and keep Enderman.hurtServer's random teleport chance off it: the thrown-potion branch teleports without it (Entity.java, DamageTypeTags, Enderman.java).
A thrown potion tested for plain water by id to hurt endermen and refill axolotls. Vanilla asks PotionContents.is for #hurts_water_sensitive_entities, #rehydrates_axolotls (and #douses_fire, #extinguishes_entities) in AbstractThrownPotion. Add the lookup over the data pack's tags/potion and use it for the two checks that exist.
A sign side had no way to say whether its text carries a run_command click event. SignText.hasAnyClickCommands (SignText.java:120) is true when any line's style has a RunCommand click event. Horizon reads the saved click_event the way ClickEvent.CODEC does for the three actions a sign acts on (run_command, show_dialog, custom).
Horizon flattened every sign line to plain text, so a click event in the text was lost the moment a sign loaded. SignText keeps each line's Component and its Style; the sign now keeps each line's click_event next to its text, writes it back with the line (an edited line keeps its style, SignBlockEntity.updateMessages), and reads and writes the allow_op_features flag of SignBlockEntity.loadAdditional and saveAdditional.
Wet or dormant sulfur never made anyone nauseous, periodic geysers never erupted, and continuous ones neither pushed entities nor showed their plume. Run PotentSulfurBlock.getTicker's tickers by state (PotentSulfurBlockEntity.java): the server gives nausea every 10 ticks, counts the geyser down with its positional random and flips dormant and erupting, and throws mobs, items and primed TNT up the column; the client releases gas clouds, draws the plume and plays its sound from the eruption block event, and pushes the local player, who moves on its own client. A window that simulates the world itself does both.
Potent sulfur had no block entity of its own here, so no countdown was kept or saved and a geyser that began erupting made no sound or block event. Add PotentSulfurBlockEntity's waitingCountdown (saved as countdown) and eruptionTick, create it with the block and keep it through saves. PotentSulfurBlock.onPlace now queues the eruption block event, plays the start sound and signals block_activate for erupting and continuous states, triggerEvent reports the event handled, and updateShape resets the countdown when a block that was no geyser turns dormant (PotentSulfurBlock.java validBlockState).
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 95c234cc1 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
HumanoidMobRenderer.getArmPose gives an arm holding a spear the SPEAR pose, which AnimationUtils.animateZombieArms leaves alone; the mobs here drew zombie or piglin arms around it. Zombies, husks, zombie villagers, zombified piglins and piglins now hold the spear out and charge it by their use time, and a piglin admiring gold no longer counts as using the spear in its main hand.
Zombies, husks, zombie villagers and zombified piglins spawned with a spear just melee. SpearUseGoal (priority 2, and 1 for zombified piglins) walks to 10 blocks, lowers the spear for KineticWeapon.computeDamageUseDuration (halved, the goal ticks every other tick), charges along the target, repositions 6 to 7 blocks away, then backs off 9 to 11 blocks (LandRandomPos.getPosAway with a minimum distance) for up to 50 goal ticks. A mount's chargeSpeedModifier and the 2 blocks a passenger adds are kept.
Goat.calculateFallDamage is the base damage minus 10, as Frog's is minus 5. Goats now leap down to five blocks and the arc lifts them higher still, so without it every ledge jump would hurt them.
GoatAi.initLongJumpActivity adds LongJumpMidJump and LongJumpToRandomPos (5 wide, 5 high, velocity 3.5714288) to the activity that runs after the ram activity and before the idle one: no tempting player, no breed target, no walk target and no LONG_JUMP_COOLDOWN_TICKS. It lands on LongJumpToRandomPos.defaultAcceptableLandingSpot (a solid block below, no path malus).
LongJumpMidJump.stop slows a mob that lands to a tenth of its horizontal speed (multiply(0.1F, 1.0, 0.1F), the float widened to a double), plays the activity's landing sound at volume 2 (GoatAi: entity.goat.step), stands it up and samples the cooldown. A goat has no swim activity, so water does not end its flight.
LongJumpToRandomPos.tick plays the activity's jump sound at the leap. GoatAi.initLongJumpActivity gives goats entity.goat.long_jump and screaming goats entity.goat.screaming.long_jump; the arc, the 40 tick wait and the jump boost are the same as the frog's.
An allay never learned a liked note block, so it always brought items to the player. AllayAi.hearNoteblock (AllayAi.java:108), called from Allay.VibrationUser.onReceiveVibration on NOTE_BLOCK_PLAY, stores the block as LIKED_NOTEBLOCK_POSITION with a 600 tick cooldown and renews the cooldown when the same block plays again.
Allay.VibrationUser.canReceiveVibration (Allay.java:564) refuses vibrations without AI and, once a note block is liked, accepts only that block while the allay is within 1024 blocks of it in its dimension. Horizon accepted every vibration.
The liked note block slot (extra.home) was never set and was a bare block position. Vanilla keeps LIKED_NOTEBLOCK_POSITION as a GlobalPos (GlobalPos.java:31) and AllayAi.getItemDepositPosition drops it when it is over 1024 blocks away, in another dimension, no longer a note block or its cooldown ran out; the throw target is BlockPosTracker(pos.above()), the centre of that block. Saved in Brain.memories like vanilla.
An allay never listened for vibrations, so note blocks played near it went unnoticed. Vanilla gives it a VibrationSystem listener (Allay.VibrationUser, 16 blocks, GameEventTags.ALLAY_CAN_LISTEN = note_block_play) ticked after super.tick() in Allay.tick, saved under "listener".
The map still said a golden dandelion did nothing: drop the missing rows for
AgeableMob.mobInteract, canUseGoldenDandelion, setAgeLockedData, setAgeLocked,
makeAgeLockedParticle and Tadpole.setAgeLockedData (the code now cites them),
and say AgeableMob.defineSynchedData sends AGE_LOCKED too.
Using a golden dandelion on a baby animal did nothing. AgeableMob.mobInteract (reached by Animal, Dolphin, SulfurCube and the other subclasses after their own checks) runs setAgeLocked with setAgeLockedData and returns SUCCESS when canUseGoldenDandelion holds; Tadpole.mobInteract does the same. A scared panda or one on its back (Panda.mobInteract) and an armadillo in its shell (Armadillo.mobInteract) answer first. A server's client treats the click as taken like the server does (AgeableMob.java:68).
Golden dandelion consumption, persistence and the use or unuse sound were missing. AgeableMob.setAgeLocked runs setAgeLockedData, consumes one item (not in creative), setPersistenceRequired when the mob is now locked, and plays golden_dandelion.use or .unuse at the block position (middle of the block, 1.0, 1.0, PLAYERS) (AgeableMob.java:88).
AgeableMob.canUseGoldenDandelion(item, isBaby, cooldown, mob): the held item is a golden dandelion, the mob is a baby, its ageLockParticleTimer is 0 and its type is not in the cannot_be_age_locked entity tag (zombie horse, skeleton horse, villager) (AgeableMob.java:78).
A golden dandelion on a tadpole did nothing. Tadpole.setAgeLockedData sets AGE_LOCKED to its opposite, setAge(0) (back to the start of its 24000-tick growth) and ageLockParticleTimer = 40 (Tadpole.java:168).
Nothing toggled AgeLocked in play. AgeableMob.setAgeLockedData sets AGE_LOCKED to its opposite, setAge(getBabyStartAge()) (-24000, a sniffer's -48000, through the age boundary) and ageLockParticleTimer = 40 (AgeableMob.java:82).
There was no age lock particle timer, so no pause or reset growth particles. AgeableMob.makeAgeLockedParticle runs at the end of AgeableMob.aiStep and Tadpole.aiStep: while ageLockParticleTimer is above 0, every even tick it sends one PAUSE_MOB_GROWTH (locked) or RESET_MOB_GROWTH particle at getRandomX(1.0), getRandomY(0.2) + bbHeight (+ 0.2F when locked), getRandomZ(1.0), then counts down (AgeableMob.java:225).
Nothing honoured AgeLocked in the shared age tick: a locked baby kept growing, a locked baby fed food still aged, and the lock never reached remote clients. AgeableMob.aiStep only counts a baby up when canAgeUp (isBaby && !isAgeLocked), and only for AgeableMob subclasses (it was also counting a tadpole's own age back down, so it never grew up). Animal.mobInteract, Panda.mobInteract and Dolphin.mobInteract feed a baby only when canAgeUp. AgeableMob.AGE_LOCKED (entry 17 after DATA_BABY_ID) and Tadpole.AGE_LOCKED are synched; MushroomCow.DATA_TYPE moves to 18 behind them (AgeableMob.java).
PiglinAi.initRideHoglinActivity lists Mount, SetEntityLookTarget(loved item, 8.0), a look at what it sees while a passenger and DismountOrSkipMounting; it has no idle movement or repellent avoidance, so a piglin heading for a hoglin no longer wanders off or strolls. A riding piglin looks at a player holding gold within 8 blocks, not 14.
A baby piglin stayed on its mount forever. PiglinAi.updateActivity ends with stopRiding when RIDE_TARGET is gone and PiglinAi.isBabyRidingBaby (a baby piglin on a baby piglin or baby hoglin) holds, so a ride lasts as long as the 10 to 30 second memory.
No dismount rule ran. DismountOrSkipMounting keeps the vehicle (or else the ride target) only while it is alive and closer than 8 blocks and PiglinAi.wantsToStopRiding says no: the vehicle is grown, dead, the piglin was hurt in the last 40 ticks (HURT_BY), or the vehicle is a piglin that rides nothing itself. Otherwise the piglin gets off and forgets RIDE_TARGET.
No brain sent a mob to another to climb on it. Mount.create (ride activity) does nothing for a passenger, boards the ride target once closer than a block, and otherwise looks at it and sets a 0.8 speed walk to it; MoveToTargetSink ends that walk a block from the target's block and aims again when it moved over 2 blocks.
Nothing ever set RIDE_TARGET. PiglinSpecificSensor remembers the nearest visible baby hoglin, and PiglinAi.babySometimesRideBabyHoglin (idle activity) copies it into RIDE_TARGET for 10 to 30 seconds once SetEntityLookTargetSometimes.Ticker lets a baby through, every 10 to 40 seconds, with draws from the level random.
Brain activities wait on their memory: PiglinAi.getActivities lists a RIDE activity that is valid while RIDE_TARGET is there, between celebrating and idle in PiglinAi.updateActivity's order. RIDE_TARGET holds a mob and expires like any memory with an expiry.
PiglinModel extends HumanoidModel, whose setupAnim bends the legs and arms of a passenger; piglin_pose never set the riding flag, so a piglin on a mount stood stiff.
Only spawning put a mob on another; no brain could. Entity.startRiding checks the boarding cooldown, a free seat and no riding cycle, and Piglin.startRiding sends a baby piglin that boards a hoglin to the top of up to three piglins stacked on it. LivingEntity.stopRiding sets the passenger down at Animal.getDismountLocationForPassenger. A hoglin seats its rider at 1.49375 (0.875 for a baby), a baby piglin hangs 0.1875 below its seat (Piglin.BABY_DIMENSIONS).
Two additions to Piglin's per-tick state each counted the same memory down, so the HUNTED_RECENTLY wait that setAvoidTargetAndDontHuntForAWhile and StartHuntingHoglin start (30 to 120 seconds, PiglinAi.TIME_BETWEEN_HUNTS) ran out twice as fast.
Piglins never turned to look at a player holding a gold ingot or another loved item. The idle and celebrate activities of PiglinAi.java start with SetEntityLookTarget on a visible player within 14 blocks for whom isPlayerHoldingLovedItem holds; add that look as a goal.
Idle hoglins ignored adult piglins and let them walk right up. HoglinAi.initIdleActivity walks an adult away from the nearest visible adult piglin at 0.4 once it is closer than 8 blocks, when it has no walk of its own (SetWalkTargetAwayFrom in HoglinAi.java).
Piglins and hoglins played their angry grunt whenever they had a target and otherwise the plain ambient one. Piglin.getAmbientSound and Hoglin.getAmbientSound play the sound of the activity the mob is in (PiglinAi.getSoundForCurrentActivity, HoglinAi.getSoundForCurrentActivity).
A hoglin or piglin that started a fight, a retreat, a celebration or an admiring said nothing; only the ambient roll played. HoglinAi.updateActivity and PiglinAi.updateActivity make getSoundForActivity's sound (retreat, angry, admiring, celebrate, jealous) when the activity changes (Hoglin.java, Piglin.java).
A hit piglin ended its celebration and, for a player's hit, its admiring, but babies fought back instead of fleeing, an avoid target of another kind stayed, and an adult piglin took on hoglins it should retreat from. PiglinAi.wasHurtBy (Piglin.hurtServer) drops the admiring and the celebration for any attacker but a piglin, sends a baby into a 100-tick flight and calls the adults, retreats an adult from hoglins that outnumber piglins, and lets it retaliate only when maybeRetaliate allows.
A hit hoglin only ended its pacified state and let the hurt-by goal target the attacker, so a baby never fled and an adult took on piglins and hoglins it should leave alone. HoglinAi.wasHurtBy (Hoglin.hurtServer) sends a baby into the retreat and lets an adult retaliate only when maybeRetaliate allows it.
A hoglin that hit a piglin kept fighting even when piglins outnumbered the hoglins around it. HoglinAi.onHitTarget (Hoglin.doHurtTarget) sets the piglin as its avoid target and broadcasts the retreat to the adult hoglins it sees.
A hoglin or piglin that decides to retreat had no way to set the avoid target, pick the nearest of its targets or warn the adults around it. Add HoglinAi.setAvoidTarget, PiglinAi.setAvoidTargetAndDontHuntForAWhile, retreatFromNearestTarget and broadcastRetreat; the broadcast reaches the other mob as an event that its next tick handles before its goals.
Hoglins and piglins never ran from piglins, hoglins or zombified mobs through an avoid target: baby hoglins panicked instead and piglins used a plain avoid goal. Add the retreat activity's walk away (HoglinAi.initRetreatActivity at 1.3 and 15 blocks, PiglinAi.initRetreatActivity at 1.0 and 12), the copies of a nemesis or zombified mob into AVOID_TARGET (PiglinAi.java), wantsToStopFleeing and the outnumbering tests, and keep the idle behaviours and the attack goals out of the retreat.
A mob that held a spear could not use it: nothing started its use or ran KineticWeapon.damageEntities for it. A mob's use now counts ticks like the player's, stabs along its view from delay_ticks on (conditions scaled by 0.2, reach by the AttackRange mob_factor), hurts and pushes the player or mobs hit, dismounts riders and broadcasts entity event 2 (LivingEntity.stabAttack, causeExtraKnockback). A piglin's admiring is told apart from a spear in use by the off hand.
Potent sulfur geysers draw four new particle types and two emitters, and none existed. Add noxious_gas, noxious_gas_cloud, geyser, geyser_base, geyser_poof and geyser_plume the way the vanilla providers build them (GeyserEruptionParticle.java, GeyserPlumeParticle.java, GeyserBaseParticle.java, NoxiousGasParticle.java, NoxiousGasCloudParticle.java): the gas fades from half its life, the base puffs burst upward, the plume shoots up and falls back, and the cloud and the eruption spawn the rest every tick. The cloud asks PotentSulfurBlockEntity.canBeReachedByNoxiousGas, ported with the other level reads of that class in the new potent_sulfur module. Particle packets with geyser data now map to and from the options.
A glow squid was lit like any mob, so it never glowed in the dark. GlowSquidRenderer.getBlockLightLevel lerps 1 - darkTicks / 10 over 0..15 and uses 15 outright, else the larger of that and the usual block light (GlowSquidRenderer.java:26).
A glow squid kept no dark timer and shed no glow. GlowSquid.aiStep runs after Squid.aiStep on the server and on clients: it lowers a positive dark tick count by one and calls addParticle(GLOW) at getRandomX(0.6), getRandomY(), getRandomZ(0.6) (GlowSquid.java:83). Only a client's level draws it; the server throws the same three random doubles.
A hit glow squid never got its dark timer, so it did not dim. GlowSquid.hurtServer sets the dark ticks to 100 once Squid.hurtServer took the damage (GlowSquid.java:97).
The stab's entity search (ProjectileUtil.getHitEntitiesAlong) lived in the game crate's spear module; mobs that stab need it from the client library, so it moves to the spear module there and the game keeps using it.
Horizon only had getPosAway on land. Vanilla's LandRandomPos.getPosTowards (LandRandomPos.java:23) takes the direction to the point and goes through getPosInDirection, a quarter turn (float cast pi/2) either side. getPosAway now shares that helper, so it uses the same float-cast angle.
Piglins and hoglins had no brain memories: nothing counted the adult piglins and hoglins in sight (HoglinSpecificSensor, PiglinSpecificSensor), kept an avoid target with an expiry, or noted which activity they were in. Add them as mob state ticked from customServerAiStep (Hoglin.java, Piglin.java).
LongJumpToRandomPos.checkExtraStartConditions needs the mob on the ground and out of water, lava and honey, and otherwise sets LONG_JUMP_COOLDOWN_TICKS to half a sample of TIME_BETWEEN_LONG_JUMPS (GoatAi: 600 to 1200). A goat has no swim activity to keep it out of the check, so it runs in water too.
A glow squid had nowhere to keep DATA_DARK_TICKS_REMAINING: nothing held it, sent it to clients, or saved it. GlowSquid defines it as the first entry after AgeableMob's two (index 18) and writes DarkTicksRemaining (GlowSquid.java). It lives in the mob's counter; the server sends it as synched data, a client applies it and the save keeps it.
LongJumpToRandomPos.start lists every block within the activity's width and height of the mob (GoatAi: 5 and 5, so 11 x 11 x 11; the frog's is 9 x 5 x 9). A goat's activity is a plain LongJumpToRandomPos, so it skips LongJumpToPreferredBlock.start's draw from the mob's random.
LongJumpToRandomPos and LongJumpMidJump serve both mobs in vanilla. The goal now sets the friction flag and the pose through helpers that know the goat's state, and the default landing spot and the cooldown count down (CountDownCooldownTicks) can be used by a goat. Frogs behave as before.
A goat that long jumps takes Pose.LONG_JUMPING, which Goat.getDefaultDimensions scales by 0.7, and LongJumpUtil checks the arc with that pose's box (LongJumpUtil.isClearTransition). The pose goes to remote clients like the frog's, and the goat holds LivingEntity.discardFriction.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 0771f59bc in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 52cc53a4f in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
Main's #3401 drew blocks through tinted_block_model_vertices with a per-face
tint closure; block_model_vertices now passes the default tints there
(grass colour once per call). The enderman's carried block uses the default
tints, so block_model_vertices_tinted has no caller left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1a01f5ef5 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
A client of a server has no live biome features, so the behaviours'
isRainingAt answered false and a Riptide trident never started charging in
rain.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: merging the short roadmap (#3859 step 4): verify green, roadmap tests and the online check pass. Proposed owners can change their line with a one-line PR.
Kira's Discord /claim marks an issue with the `in progress` label instead of
assigning it; `/claim` on GitHub now refuses such an issue, so the two
claim paths can't hand the same issue to two people. Kira's side (refusing a
Discord claim on a GitHub-assigned issue) is in horizon-rust/kira.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The website renders the "Project status" intro with its own markdown, which
prints HTML comments as text: the block is now found by its first words.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity_gaps.py counts the rows not yet ticked in the port ledger's
ten gap lists (#3512-#3521) and writes them first under "Project status"
(by area, linked) and in a badge beside the 100% one. The website shows the
block already: it renders that section's intro. Today: 833 open of 833.
Refreshed in the weekly roadmap pass (roadmap/README.md); tests check the
block, the badge and the site's heading agree, without the network.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
roadmap.toml items get `owner` (a GitHub login) and `issue` (the tracking
issue); `check` requires both on every Now item, at most 8 Now and 12 Next
items, and the tracking issue to be an open issue. ROADMAP.md shows them,
roadmap.json carries them (the hub ignores unknown keys).
The proposed lists, from the work actually happening (open pull requests,
agents' workstreams):
- Now: smooth-frames, server-tick, mods, port-ledger (the player-visible
gaps), modpack-worldgen, server-thread, java-instances, lean-code, with
tracking issues #3909-#3916.
- Mods: one line (Ludovic's decision), owner levzzz5154; native-mods,
mod-registry, server-mods, mod-screens, modder-tools, machines-api and
mod-world-items fold into it.
- Shipped (`sync`, all their pull requests in a release): server-dialogs,
discord-presence, native-shaders, pregen, item-cooldowns, chat-settings,
pointer-hand, text-field-keys, chicken-flap. phones-and-browser is not
shipped: it becomes ongoing so `sync` stops taking its first pieces for it.
- Next: 12 (splitscreen, owner martipops, and launcher-redesign come up);
the rest of Next moves to Later.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleUpdateAttributes gives every living entity its
attributes; the client kept only a mob's max health, so a server's scale
on another player or a mannequin was dropped and NPC plugins' enlarged
fake players showed at their normal size. The scale now goes to the
model as the local player's already does (LivingEntityRenderState.scale),
computed like AttributeInstance.calculateValue, now a function with a test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Component.tryCollapseToString: ComponentSerialization.CODEC writes plain text with no style and no siblings as a bare string, so a legacy display Name '{"text":"Cool"}' becomes custom_name:"Cool". Keeping the compound made 215 of hotbar's 905 cases differ.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A camel kept its pose change as a signed age, so a camel finalized at age 0 had pose time 0 and could not move for 52 ticks, and LastPoseTick was saved as a sign. Keep a signed age anchor plus a sitting flag, and save and load LastPoseTick as a game time.
ItemStackTemplate.CODEC bounds the count with intRange(1, 99). Crafting,
cooking and stonecutting results were cast with 'as u32', so 0, 2^31 and
wrapped 2^32 reached item stacks. Such recipes are now not loaded.
🤖 Agent: merging the kind and area labels (#3859 step 3): verify green, workflow-safety and labelling tests pass; labels created and the open pull requests labelled once.
upgrade_world wrote level.dat at the current DataVersion before converting chunks, so a failed conversion was never retried. The level.dat write now comes last.
On open and push, overlap-note.yml (pull_request_target, main's script only)
lists, in one comment edited on every push, the other open pull requests that
close the same issue or change the same functions. Functions come from git's
own rust/python hunk headers, which name indented methods (GitHub's stop at
`impl Game<'_>`). The heads are fetched as objects and diffed, never checked
out; hooks, fsmonitor, external diff and textconv are off.
Measured on the 207 open pull requests: one shared function, or a few of a
big pull request's hundred, was mostly chance, so a pair needs a shared
issue, or 2 shared functions and a tenth of the bigger one's; functions 5
compared pull requests change, probes, tests and stacks don't count.
21 pairs (34 notes): 16 related (4 duplicates), 4 doubtful, 1 unrelated.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ModelEngine names a bone's item model `modelengine:<model>/<bone>` and
ships it at assets/modelengine/items/<model>/<bone>.json. Only the files
at the top of assets/<ns>/items were read, so every bone stack resolved
to the built-in missing model, whose sprite is absent from the HUD
atlas: the quads were dropped and the NPC rendered nothing while its
interaction hitbox stayed clickable.
Walk the tree like ParsedModels::load already does for models.
Co-Authored-By: Qoder <[email protected]>
effect_modifiers only mapped strength and speed, so Weakness and Slowness
had no effect on a mob: its melee damage and movement speed stayed at the
base values. Map every attribute-carrying effect the way vanilla's
MobEffect.addAttributeModifier does, scaled by amplifier + 1:
- weakness: attack damage -4 per level (ADD_VALUE)
- slowness: movement speed -0.15 per level (ADD_MULTIPLIED_TOTAL)
- health_boost: max health +4 per level (ADD_VALUE)
Move the per-effect table into a testable effect_modifier() and cover all
five rows with a unit test.
Fixes#3887.
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
Entity.turn adds the turn to xRotO as well as xRot, so the local
player's getXRot(partialTick) follows the mouse every frame. Every turn
(mouse, pad, arrow keys, the control `turn`) now goes through
turn_player, which moves both ends of the model's head pitch and ends
in AbstractBoat.onPassengerTurned's clamp. handleRotatePlayer snaps the
model (setOldRot), and handleMovePlayer moves its old pitch by the
packet as setOldPosAndRot does. A change the tick makes still eases in.
Fixes#3844
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.rideTick sets the boat's input after the boat ticked, so
AbstractBoat.tick steers with the previous tick's keys and the paddle
packet it sends describes the motion it just made. Horizon set the keys
before ticking, a tick ahead of that packet (GrimAC Simulation flags).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
issue() wrote the body file in the system's code page, while gh reads
it as UTF-8, so on Windows a body with "→" raised UnicodeEncodeError
(cp1251 here) or would reach GitHub garbled. #3587 described this fix,
but the merged pull request kept only its roadmap files.
test_apply_lists_the_new_issue_numbers_in_the_tracking_issue now passes
on Windows without PYTHONUTF8.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The scrubber puts <user> in place of the folder name in /Users/... and
C:\Users\... paths, then replaces the machine's user name wherever it
stands alone. When that name is "user", the second pass matched the
word inside the label it had just written, so a crash report read
/Users/<<user>>/... and telemetry's a_crash_keeps_only_what_says_what_happened
failed on such machines (seen in #578 and #3838). A match between "<"
and ">" is now left as it is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On macOS and Windows nothing calls it, so clippy -D warnings failed there
(dead code) while Linux CI passed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by server-tick-1: due scheduled ticks run in one with_level while a step leaves no level events or spill (identical order and state); tick_blocks span 15.4/14.8 → 10.4/9.3 ms at 50 bots; pr_ready --quick green; merging once CI parity passes.
On the server thread (the default) breeding runs in the mob tick, outside
any player's turn, and the server has no host player, so the
bred_animals trigger and the animals_bred statistic were dropped and
"The Parrots and the Bats" was never granted.
Remember who fed each animal into love (Animal.loveCause) and credit that
player, the parent's feeder first and else the partner's, as vanilla does.
On the server thread (the default single player), Save & Quit goes
through leave_server, which stopped the integrated server but kept the
sharing state: back in a world, the old code still showed with nothing
behind it. leave_server now ends the code, as lan_close already did for
the in-frame path. test_friends checks it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
F2 was handled in `Game::key_pressed`, which a screen with a text box never
reaches: chat, a sign, a book, the command block and the search boxes take the
key first, so F2 did nothing there (#3689).
Vanilla runs `Minecraft.handleGlobalKeyPress` before a screen sees the key:
`key.fullscreen` and `key.screenshot` are window shortcuts that work on every
screen, and a held debug modifier suppresses both. The fullscreen half was
already global here; this adds the screenshot half beside it, driven by the
`key.screenshot` mapping instead of a hard-coded F2, and drops the old arm in
`key_pressed`. `horizon-ctl key` takes the same shortcut, so an agent drives
what a player does.
Co-Authored-By: DeepSeek V4.1 Flash (opencode) <[email protected]>
The template gets a "Kind" list (parity, bug, perf, feature, extra, tooling,
docs). pr-labels.yml (pull_request_target, main's script only, never the
pull request's code) sets the ticked kind when the pull request has none, swaps
it when the author changes the tick, and never removes a label set by hand.
Area labels come from the changed paths: at most 3, each a tenth of the lines.
`feature` keeps its release meaning.
pr_labels.py also creates the labels and labels the open pull requests once
(backfill: kind only where clear, else none).
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
IntegratedServer.initServer uses authentication, so a vanilla LAN is in
online mode: each guest's hasJoined answer brings their signed textures
property, and everyone sees their skin and cape. Horizon's LAN stayed in
offline mode, so guests joined with no properties and showed as default
skins without capes.
OnlineMode::lan() makes the key pair when an account is signed in (an
offline name keeps offline mode), for the server thread and the in-frame
host alike; friend codes reach the same port through the tunnel. As in
ServerLoginPacketListenerImpl.handleHello, the integrated server's own
player over memory is never asked to authenticate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In vanilla Minecraft (26.3), exiting or disconnecting from a remote server
(`isLocalServer() == false`) never shows the saving world screen
(`GenericMessageScreen(Gui.SAVING_LEVEL)`). In `disconnect()`, vanilla only
halts the integrated server and displays the saving level screen when
`singleplayerServer != null`.
Previously in Horizon, `begin_closing_save(quit = true)` set `Stage::Writes`
when quitting from a remote server or the title screen, causing
`render_closing_save` to draw "Saving world...", and `save_for_exit` called
`save_game()` on exit even for remote worlds.
- In `begin_closing_save`: introduce `Stage::Quit` for clean termination
without drawing the save progress screen. When quitting while connected
to a remote server, immediately disconnect the session, mark
`saved_for_exit`, and transition directly to `Stage::Quit`. Quitting
from the title screen also uses `Stage::Quit`.
- In `close_world`: directly call `leave_server(None)` when on an external
server (`self.external_server()`), skipping `begin_closing_save`.
- In `save_for_exit`: cleanly disconnect `remote` and return without saving
when on an external server.
Co-Authored-By: Antigravity <[email protected]>
The trash crate starts COM on the calling thread and panics when that
thread already runs COM in the other mode (RPC_E_CHANGED_MODE). Deleting a
world runs on a worker of GPUI's thread pool, which can, and a panic in that
callback cannot unwind, so the launcher aborted and the world stayed.
The system trash now runs on a thread of its own, and a panic there comes
back as an error.
Fixes#3784
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Anyone comments `/claim` on an issue to be assigned (GitHub lets an issue's
commenters be assigned, no role needed); an assigned issue is taken.
`/unclaim` gives it back; the trusted reviewers can `/unclaim @login`.
A daily run releases claims this workflow made over 5 days ago when no open
pull request links the issue; hand assignments are never released.
Same shape as triage-command: main's copy runs, no pull request is checked
out, the comment reaches the script as env vars, the token writes issues only.
Part of #3859.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified by pr-triage-3. Dev tooling only (tools/port_ledger.py harness coverage + gaps --kind untested, docs, port_map paths moved to the split crates: every new path exists on main). Merges cleanly with main; tests/test_port* 30 pass; skip-release.
The scenario depended on the wither staying put for the 20 ticks before
destroyBlocksTick fires. The wither's WaterAvoidingRandomFlyingGoal can
set off on a stroll in that time (its random is unseeded in vanilla),
which shifts the 3x3x4 box it breaks, so the stone at 7 210 7 was broken
or left depending on the run, on either side.
Teleport it back to 8.5 210 8.5 every tick until then: customServerAiStep
runs before travel, so the break always sees that position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A retracting piston is a moving_piston block for two ticks. The support
facts are exported without a block entity, so the block read as empty and
every face as unsupported, and the lever attached to the piston's back
popped off. Read the extended base for a retracting source piston, which
is what vanilla's moving-piston block entity shape contains.
ludovic111 pointed out that the first pass's RD 8 and RD 32 games had builds
running alongside, which docs/benchmark.md keeps out of published results.
Each game now started only with no compiler or other game running and the
CPU under 15 % for 30 s, and a game during which one started (or other
processes used over 15 % for 10 s) was run again. RD 16 was already idle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The glyph loop takes both: an inline object draws and skips the shadow passes; other glyphs use the component's shadow_color. Both sides' tests kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ArmorStandRenderer adds WingsLayer after the armour layers, but
armor_stand_geometry never drew the chest slot's wings. Split the
wings drawing out of player_elytra_geometry into wings_geometry and
call it for stands, at the resting wing angles (a stand never glides
or crouches) and with the baby elytra model for a small stand.
Mob::add_effect normalizes ids to their minecraft: form (08b97652), but the
test asked has_effect for the bare "glowing", so the assertion failed.
Fixes#3840
Language::ALL holds eight and every list of codes names pl. Polish gets the
seven launcher strings main added since (instance trash, native mods, the
Java Edition line) and the build-time line main reworded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A chunk's summary kept the chunk's own light, which fades a level per block
of water, while the seed's far columns light water and its floor as open
sky. The chunks a player passed then drew a darker band through the far sea.
Summaries now light open water and its floor as open sky, and the water is
one span again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
check_eating (each player eats a loaf in front of the other) now also
counts entity.generic.eat on both sides: the eater and the watcher must
each hear one bite per crumb burst the eater draws, plus
FoodProperties.onConsume's sound, for the loaf eaten (#3129).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`BaseSpawner.serverTick` returns first thing without an alive player within
its range (`isNearPlayer`) or with `spawner_blocks_work` off, before any
draw. Each spawner still built the spawn code's level every tick for it:
every player with its box and mode, the nearest-player list and the mob
world. Ask the players directly first, without collecting them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/ride a text_display mount @s makes the window's own player the
vehicle, which isn't in mobs: the display stayed where it spawned. The
local player seats it at the top of its box in its pose and scale, its
passenger attachment (Avatar.POSES), as for other players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The capture fixture compiles the client's own capture body; it now has
the game's options, and an offscreen game must keep its grab as its own
state without touching the window's pointer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New `mooshroom` check: a mooshroom summoned with Type "brown" is drawn
brown on the host and the guest, and a red one a bolt strikes
(MushroomCow.thunderHit) turns brown on both screens, with the server's
Type "brown". DATA_TYPE is synched data entry 18 (#3236).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An offscreen game renders into a texture, so its window only ever showed
white, yet entering a world grabbed the system pointer: on Windows it was
clipped to one pixel even while the window wasn't in front, until the
person alt-tabbed. The window is now created hidden, the grab is the
game's own state (agents still turn the view with mouse-move), and the
real mouse's raw motion never steers an offscreen game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`tick_scheduled` built the level (`with_level`: the boxes of every cart,
arrow and guest, the fire-spreading players, the game rules, the ticket
levels) again for each due block and fluid tick. Run the due ticks in one
`with_level` while each leaves nothing for the game to apply between
them, level events or a spill; then the game handles those as before, so
every tick still sees what the ones before it did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: the creative screen's tooltip takes main's tab names
(draw_stack_tooltip_with) and this branch's TooltipFlag.isCreative for a
grid slot; horizon-protocol's item.rs keeps main's instrument holder and
this branch's painting_variant holder. The new wire test's Dynamic gets
main's dialog_tags by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts kept from both sides:
- mobs/ai.rs: parrots register through parrot::register (flight), next to
main's own rabbit and ocelot arms.
- tools/test_mob_ai_parity.py: the parrot scenarios and main's wolf_hurt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolved zip.rs: main added a_name_falls_back_to_its_folder_entry where this
branch added its deflate tests; kept both.
Co-Authored-By: Qoder <[email protected]>
main's content mod docs (items patch their template's components) kept; its
change to the WebAssembly API reference goes with the page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
UI Brightness takes params[6]; the tier lift also skips pixels the hand or
HUD covers, as main's lift does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The owner's login to its server thread is main's again: no properties
and no wait. The player list gives the integrated server's owner the
signed-in account's properties instead (Hub::player_info), taken from
the background fetch only if it has already answered, so a slow, failing
or absent network can't delay opening a world. A LAN guest joins after
the world is open, well after the fetch.
Tests: the owner's login doesn't wait on a fetch that never answers; the
player list's owner entry before, after and without the fetch; and in
tests/lan_bot.rs (needs the prepared resources) a TCP guest's player
list carries the owner's textures and none of its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CxQvrTahuwEpRxJpTvWpjG
Both menus only closed from their own button or a row. They now close on
a mouse-down anywhere outside them; a press that does so on the menu's
own button no longer reopens it when it ends. Fixes#3216.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep main's Component::Instrument beside this branch's StewEffects (enum,
test and item_wire_probe).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player-facing notes used GITHUB_REPOSITORY, so fork CI rewrote the
launcher releases href and failed
test_a_game_release_opens_with_the_way_to_the_launcher.
On Windows npx is npx.cmd, and subprocess (CreateProcess) does not find
it by name: the script stopped with FileNotFoundError. Resolve it with
shutil.which, as the script already does for rea, and say what is
missing when Node.js isn't installed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gave overlay 35 to text drawn with a polygon offset (TEXT_POLYGON_OFFSET_OVERLAY,
its own material in gfx/entity_batch.rs), so armour with 35 would be drawn as that
material. The test now lists it among the overlays the glint must not take.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockGetter.traverseBlocks visits the block a ray starts in first, and
VoxelShape.clip hits a shape that contains the ray's start. pick_fluid
began stepping from the eyes' block without testing it, so under water
a bucket took the next source along the ray (the feet's, looking down)
and no air pocket could be made. Fixes#3787, #3233.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
worldgen/block_state_provider was missing from overrides::KINDS, so a
pack's own providers (WWOO's wythers:stony_cliffs) never reached the
state_providers bundle the generator already reads, and every feature
using one was skipped. pack_files_go_to_their_bundles covers the folder.
Reported by AdachiReiYay's agent on #3476.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
worldgen/block_state_provider was missing from overrides::KINDS, so a
pack's own providers (WWOO's wythers:stony_cliffs) never reached the
state_providers bundle the generator already reads, and every feature
using one was skipped. pack_files_go_to_their_bundles covers the folder.
Reported by AdachiReiYay's agent on #3476.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bd92abce3b0ba8f209f8a613acd5741cb9e4b825)
world.rs keeps main's FxHashMap/FxHashSet imports beside this branch's
SectionMask. Main's writes_mark_exactly_the_reachable_sections sorted
(column, mask) pairs, and a SectionMask has no order: it sorts by the
column, which each pair has once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column at the edge of the loaded area lights against its decorated
neighbours past the area, so the streamer turns those proto chunks into
columns (`light_view`) on the tick thread. It built them again for every
light job and for every retry of a column still waiting on its last
neighbour: on a dedicated server with 50 bots, 59 220 views for 5 345
proto chunks. Keep each view until its proto is checked out, replaced or
removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
skin_entries holds the lookup update_remote_skins does, so a test can
play an NPC plugin's add, spawn and remove: the entity keeps the skin
its entry gave, a player spawned before its entry takes it when it
comes, and a removed entity forgets it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
getSuitableFlyToPosition returns the first random position (no mirror under
the heightmap unless 64 tries fail); GhastMoveControl tests floatDuration
before decrementing it; GhastLookGoal turns with the float 180/pi and
Mth.atan2; the warning cry and the shot are level events whose pitch is the
client's, not draws from the ghast's random.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
NPC plugins add an NPC's profile to the player list, spawn it, and take
the entry off the list a moment later to hide it there. Vanilla keeps
it: AbstractClientPlayer.getPlayerInfo looks the PlayerInfo up once and
holds it, so getSkin still reads its textures. PlayerInfoRemove dropped
the profile's textures here, so on tensa.co.ua every NPC showed its own
skin for about two seconds, then a default one (efe, zuri, sunny, kai).
level_player_textures keeps, per player entity, the textures it found
first, as level_player_names already keeps its name; update_remote_skins
resolves a player whose entry is gone from them. A new entity, a removed
one and a new level forget theirs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An armor stand used the air travel in water too and ignored bubble columns,
so one standing in a soul sand column never rose. LivingEntity.travel now has
its water branch (slow down 0.8, gravity / 16 through getFluidFallingAdjusted
Movement), and the blocks the stand is inside (a bubble column) push it
between the move and the friction, as Entity.move's checkInsideBlocks does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Cat.reassessTameGoals adds the player-avoidance goal after registerGoals, so
it is evaluated last. Cat.customServerAiStep sets sprinting while the move
control's speed modifier is 1.33: +30% movement speed, and Entity.baseTick
draws twice from the mob's random for the sprint dust. The probe names the
goal CatAvoidEntityGoal as vanilla's subclass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
An empty `biome_is` list on a surface that does not number its biomes
compiled to the id comparison, since collecting no indices never asks
`biome_index`. It then failed with MissingBiomeProvider where vanilla and
main answer false (material_condition's codec-corpus condition 49). It
keeps the name lookup main uses. Review on #3366.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Move the active colormap lookup outside the model/face loops, as requested in
review. All tinted faces in this draw share the same default grass colour.
Implemented by OpenAI Codex on behalf of NetuosoDEV.
Co-Authored-By: OpenAI Codex <[email protected]>
NPC plugins hang a text display on a fake player with SetPassengers, and
vanilla seats it every tick (Entity.rideTick, positionRider) at the
vehicle's passenger attachment, the top of its box, less the display's
own vehicle attachment, which is 0. remote_set_passengers placed only
mobs and the local player, so a ridden display stayed where it spawned,
at the NPC's feet (tensa.co.ua's lobby: the glyph titles under the NPCs).
A remote text display now keeps its vehicle and position_riders seats
it after the tick, from the last tick's seat, so it follows a moving
vehicle smoothly; a teleport sent meanwhile no longer drags it off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AMD FidelityFX SDK 1.1.4's optical flow (MIT): luma pyramids, scene change
detection, and a 7-level block search with filter and scale, as WGSL compute
passes that run on WebGPU's default limits. Not used by the game yet; the
interpolation and the setting follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecartFurnace.calculateNewPushAlong projects the push onto the
movement and scales it back to the push's own 3D length. Horizon kept
the push as x/z only and normalized with the vertical part, so the
push lost length every tick while gravity gave the movement a vertical
part (the cart moved with a 0.04 per tick fall). A cart fuelled from
about half a block away slowed down to about 0.56 to 0.79 of vanilla's
distance over its 3 minutes of coal.
Keep the push as a Vec3 between ticks. Saving still writes only PushX
and PushZ, and loading and fuelling set y to 0, as vanilla does.
Vanilla: MinecartFurnace.applyNaturalSlowdown, calculateNewPushAlong,
addFuel, addAdditionalSaveData, readAdditionalSaveData;
Vec3.projectedOn, Vec3.normalize.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
/place template wrote a block's saved data over whatever block entity
was at its position and dropped the old one without removal handling.
Over the same block state, place_state returns early, so a playing
jukebox kept its song and an open container screen stayed on the
replaced entity.
StructureTemplate.placeInWorld first sets a barrier where a block with
saved data goes, which makes LevelChunk.setBlockState remove the entity
there. Do the same before placing: remove the entity and its custom name,
stop a removed jukebox (place_state does that itself when the block's
name changes) and close a container screen open on it.
Refs #3708
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
The integrated, LAN and dedicated server sent minecraft:brand "vanilla"
(MinecraftServer.getServerModName) while Horizon's client sends "horizon".
A modded server names itself, so Horizon's does too: F3 shows it, and a
server admin can tell Horizon servers apart (bunchy7s, Discord).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
load_chunk put each loaded block entity into the map over any entity
already at its position and kept that position's ticker slot whatever
the new block state was. Vanilla's LevelChunk.setBlockEntity drops the
previous entry, and updateBlockEntityTicker rebinds the ticker only when
the new state has one, removing it otherwise.
Mark replaced positions and, at the next tick, free their slot unless
the block state ticks. Add a test covering a replacement that keeps the
slot and one that frees it.
Vanilla: LevelChunk.setBlockEntity, LevelChunk.updateBlockEntityTicker,
LevelChunk.registerAllBlockEntitiesAfterLevelLoad.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
The server sent every level event of an acting player's action with that
player excluded, so a torch or flower that popped off when the player mined
the block it stood on was silent (no break sound, no particles) for the
miner. Block.playerWillDestroy excludes the player only from the destroy
events of the block it breaks (and its other half); what the break sets off
afterwards goes through destroyBlock with no player. Only the events up to the
broken block's are the player's own now.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Resolve the carried-block overlap with #2986 while preserving upstream's iron
golem flower layer, fox item context, minecart changes and renderer crate split.
Keep the captured carry transform and per-face BlockTintSource defaults; remove
the superseded single-tint helper that only the old enderman path used.
Carry the tint implementation into horizon-client-render/src/mesher.rs and
update the differential harness's source path after the upstream module move.
Resolved by OpenAI Codex on behalf of NetuosoDEV. No additional local tests
were run, as requested by NetuosoDEV; the updated PR head is checked by CI.
Co-Authored-By: OpenAI Codex <[email protected]>
ZombifiedPiglin extends Zombie and only overrides addBehaviourGoals, so it
keeps the turtle egg goal (priority 4) that Zombie.registerGoals adds. Horizon
left it out, so the goal never drew its start delay and the mob's random
stream drifted from vanilla's on its second tick.
Adds a zombified_piglin_player scenario to the mob_ai harness.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Use the baked block-state model instead of the block item's model so flowers,
fungi and roots keep their cross geometry and state properties survive. Remove
the item-centering compensation while retaining the vanilla carry transform.
Apply BlockTintSource.color(state) to tinted block-model faces using the active
grass colormap and vanilla default foliage colours. Keep in-world sugar cane
and lily pad colours unchanged. Use BlockModelResolver's fixed model seed 42.
Add a differential adapter checking 6,616 state/tint-index combinations against
Minecraft 26.3 and a scratch-world offscreen screenshot driver. The existing
captured-vertex transform regression now checks block coordinates directly.
Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV manually tested
the fix in game and confirmed it works.
Co-Authored-By: OpenAI Codex <[email protected]>
Send the command block's block-entity data when opening its screen and
apply incoming block-entity updates to the open edit screen. This makes
the command and output settings match the server instead of a stale
client copy, mirroring vanilla's openCommandBlock and
handleBlockEntityData behaviour.
Files:
- crates/horizon-client/src/game.rs
- crates/horizon-client/src/game/command_blocks.rs
- crates/horizon-client/src/game/remote.rs
- crates/horizon-client/src/lan/chunks.rs
Main's horizon-client-render split (#3186) moved chunk.rs, world.rs,
streaming.rs and gfx.rs, which this branch changes: section_mask.rs moves
with them into horizon-client-render, and horizon-client re-exports it
beside the other moved modules. lan/chunks.rs keeps both imports (main's
spawner traits and SectionMask).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Worn armour found its equipment asset only under minecraft, and the atlas
packed only minecraft's entity textures, so a content mod's armour could
not draw as itself. EquipmentAssetManager reads `equipment/` in every
namespace: the armour layers now load from the content mods' namespaces
too, under `<ns>:<name>`, and their textures pack as
`<ns>:entity/equipment/<layer>/<path>`. A root without content mods reads
exactly what it did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the oracle's facts, every item came over as a stick: a ported sword
had no damage, no durability and no mining rules. An item of a vanilla
tool or armour class now gets the iron item of its class and slot as its
template, and the oracle's durability, attribute modifiers (1.21.1's ids
upgraded), tool rules, enchantability and repair items as a component
patch, minus what the template already has. The mod's tool rules replace
the template's whole (its tier names its own incorrect-for-drops tag);
what 1.21.1 kept in the class, a sword sparing blocks in creative, stays
the template's. Simplest Emerald Gear's item stats: 4/49 -> 64/64 against
the mod on NeoForge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The items check only saw an item exist with its stack size, so a ported
sword carried as a stick passed. The probe now dumps what Horizon's loaders
make of each item (durability, attribute modifiers, tool rules,
enchantability, repair items, equipment slot, food) and the harness checks
them against the oracle's components, as `item_stats`, with 1.21.1's
`generic.` attribute ids read as 26.3's and vanilla's newer tool rules
allowed beside the mod's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each item now carries its default components (vanilla's codecs, 1.21.1's
form), its effective attribute modifiers (armour's come from a method,
not a component), enchantability and repair items, and for armour its
slot, defense, toughness, knockback resistance, equip sound and texture
layers: the ground truth an item port is compared against. The export
also creates its output folder instead of failing to write.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added block_model_vertices_tinted (one tint for every tinted face)
for mob_blocks.rs; it now calls this branch's tinted_block_model_vertices
with that tint for tint indices >= 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A zip entry named `../x` or `/x` (or `C:\x` on Windows) was written as it
was, outside `<world>/datapacks/<name>-26.3/`. `read_pack` now takes each
entry's name from `enclosed_name()` and refuses a pack with one that has
anything but plain components, and `upgrade_into` refuses to write such a
path. Review on #3369.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WitherSkeleton.registerGoals adds its AbstractPiglin target goal before it
calls super.registerGoals, so among the priority 3 target goals it is
evaluated before the iron golem and turtle ones. Horizon added it last, so
the goals drew their random intervals in another order and a wither
skeleton's random stream drifted from vanilla's on its second tick.
Adds a wither_skeleton_player scenario to the mob_ai harness.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
#3085 already moved the bow, trident, throws, potions and wind charge to
known_player_movement(), and main has the Windows test fixes (#3088, and
#3587 for port_publish.py). This branch takes main's version of all of
those. What it keeps: Guest::known_movement, set on every accepted move
and zeroed by a ClientTickEnd without one (handleClientTickEnd), and the
save and restore of Game::known_movement in enter_guest/leave_guest.
The moves.last() copies in UseItem, the spear's jab and the per-tick
using turn go, since enter_guest sets the value for every turn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Animal, AbstractVillager, AbstractPiglin and CopperGolem set FIRE to -1 and
FIRE_IN_NEIGHBOR to 16; Fox, Parrot, Frog, Goat, Wolf, Blaze, ZombifiedPiglin
and WitherSkeleton set their own. Horizon left every mob on PathType's
defaults (FIRE 16, FIRE_IN_NEIGHBOR 8), so animals, villagers and piglins
walked through fire that vanilla paths around.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Beds checked the level's raw thundering flag, whose timer also runs in
clear weather, so players could sleep through a sunny day. The Overworld's
bed_rule sleeps WHEN_DARK, as vanilla's Level.isDarkOutside: skyDarken >= 4,
from the time of day and the rain and thunder levels.
GroundPathNavigation.trimPath, with avoidSun set by RestrictSunGoal, cuts a
path that starts out of the sun at its first sunlit node, so a skeleton
chasing a player by day waits in the shade. Horizon never trimmed a path.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
App::removing held one version, and remove() returned early while it was
set, so a second row's Remove did nothing until the first delete ended.
It becomes the list of versions in flight: each row starts its own
delete, Msg::Removed drops its own tag, and the rows, Play and Install
ask whether that tag is in the list. The gate is removable(), so the
two-at-once case has a test.
Fix#3719.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
AbstractVillager.stillValid keeps a trade screen open only while the trader
lives and the player is within the entity interaction range plus 4 of its
box. Neither the host's own trade screen nor a guest's (which includes the
single-player window on the server thread) was checked, so the screen
stayed open after walking away.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Horizon built a villager's brain once, with its baby flag, so a villager
that grew up kept the baby schedule (play) and never went to work. At the
age boundary the brain is now rebuilt as Villager.refreshBrain does: running
behaviours stop, the packed memories carry over and the sensors' first scans
are delayed by draws from the mob's random.
Adds villager_grows_up scenarios to the mob_ai harness.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Text displays: main's text_display::draw and Lines (#2812) replace this
branch's inline drawing, with this branch's display_light (the same light);
this branch's display_data and transformation are kept beside them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
chicken_panic_192 fails without the Chicken water malus (nav path of 7
nodes through the pool instead of vanilla's 5).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Conflicts:
- game/stations.rs: keep both tooltip lines, the ominous bottle's before
the suspicious stew's, in ItemStack.addDetailsToTooltip's component order.
- item_tooltip.rs: keep both tests; the stew test now expects main's
zero-padded durations ("00:03", "00:07").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
- The game now reads HORIZON_OFFLINE_SKIN / HORIZON_OFFLINE_SKIN_MODEL,
which the launcher already sets for an offline name, and draws that skin
(slim or classic) on the local player where neither the server nor a
signed-in account gives one. It was read for capes only.
- On the window's own server thread (single player, and the world opened
to LAN), the owner's memory connection now logs in with the signed-in
account's profile properties (IntegratedServer.getSingleplayerProfile),
so the player list carries the owner's signed skin to LAN guests instead
of the default one. The profile is fetched while the world loads; the
login waits for it at most 10 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CxQvrTahuwEpRxJpTvWpjG
The HOTBAR tab was skipped since the creative tabs came in (#49, "Not
covered: ... the saved-hotbars tab"), while saving and loading toolbars
with C or X and a hotbar key already worked.
- CreativeModeInventoryScreen.selectTab on the HOTBAR tab: the nine
saved rows of hotbar.nbt; an empty row shows a paper in its own slot
with creative_slot_lock and the item name inventory.hotbarInfo, named
after the bound save and hotbar keys. The rows are read when the tab
is selected and when the inventory opens on it.
- CustomCreativeSlot.mayPickup: a locked stack can't be clicked.
- Creative::shown gives a grid cell's stack, None for a toolbar's empty
slot, for the clicks, the hover, the drawing and horizon-ctl.
- VanillaCreativeSearchOracle compared the tabs without the HOTBAR one,
which Horizon didn't show; it now compares every tab vanilla shows
(shouldDisplay). The tab's display items are empty on both sides: its
contents come from HotbarManager when it is selected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cod, salmon, tropical fish, pufferfish, tadpoles, squid and dolphins keep
PathType.WATER at 0 (WaterAnimal and AgeableWaterCreature constructors).
Horizon left it at 8, so DefaultRandomPos rejected every water position
and RandomSwimmingGoal retried ten times, ending on a spot in the air.
FishMoveControl also computes its yaw like MoveControl does.
Adds cod, salmon and tropical fish scenarios to the mob_ai harness; the
probe names the fish goal FishSwimGoal as vanilla does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
SpawnEggItem.useOn on a spawner takes the type from getType, the egg's
entity data id when it has one, and for an op (PlayerList.isOp) calls
Spawner.setEntityData: TypedEntityData.loadInto sets the next spawn
data's id and merges the egg's data into it. Horizon set only the egg's
own type, so a custom egg gave a plain mob.
The acting server guest's permission level is kept beside its name and
UUID (lan_acting_permission), so the check is the acting player's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Game::items`/`recipes`/`stations` fell back to themselves when the world
had no data packs instead of to `Resources.rest`, so a world without packs
recursed until the thread spun at 100%: the window never joined its
integrated server and showed "Loading terrain..." forever.
Co-Authored-By: ZCode Agent <[email protected]>
DebugStickItem: a left click selects a block-state property and a right click cycles its value (Shift reverses it), stored in the minecraft:debug_stick_state component. The client never breaks the block and swings on use; the server cycles the state and sends the block update.
The sneak direction follows the acting player's sneak input (host, toggled sneak or guest), a guest's command permission is swapped for its turn so game-master checks use the guest's level, and useOn returns the vanilla SUCCESS/FAIL, triggers ITEM_USED_ON_BLOCK on success and only swings the local actor's client.
Co-Authored-By: jaszkus's AI agent <[email protected]>
- Route sulfur cube baby flag through baby_index (entry 16) in lan.rs
- Remove client-side baby state derivation from size in remote_mob_data.rs
- Revert finalize.rs age_boundary_reached and set_baby to vanilla behavior
- Ensure spawn_egg_offspring sets size 1 on new sulfur cube offspring
- Drop lists.rs change from this branch
The first commit replaced the tick_items span with the gateway handler, so
dropped items stopped falling, merging and despawning in every mode. The
call is back, next to the gateway's.
An entity that has just come through a portal carries PortalCooldown 300.
EndGatewayBlock.entityInside still triggers the gateway's own cooldown, but
Entity.setAsInsidePortal then only restarts the entity's cooldown, whatever
the portal: the gateway now asks the shared portal state first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server only hears the movement keys (ServerboundPlayerInputPacket), and
vanilla's client moves exactly as they do (KeyboardInput.tick: 8 directions,
full speed). With analog movement a player walked at angles and speeds the
keys can't produce, and a server that predicts movement from them could pull
it back. On a server that isn't this game's own server thread the stick now
moves as its keys; single player keeps analog movement.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82HU35NWsN14K4taHT
cmd_damage only handled the window's own player and mobs: any other player
(a LAN guest, a dedicated server's player, a splitscreen player) fell through
to "Target is invulnerable". Run it as that player through for_player, as
/effect does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82HU35NWsN14K4taHT
A water bucket fills the clicked LiquidBlockContainer (stairs, slabs, fences, trapdoors and other SimpleWaterloggedBlocks) instead of the block next to it, and an empty bucket's SOURCE_ONLY ray now targets waterlogged blocks too, so their water can be picked up again. The empty-bucket use also reports the vanilla FAIL/PASS result, applies the sneak rule, keeps creative's filled-result behaviour and lets only creative players waterlog a barrier.
Co-Authored-By: jaszkus's AI agent <[email protected]>
Consumable.emitParticlesAndSounds plays its bite through
Player.playSound, which is level.playSound(player, ...): the server
sends it to everyone but the eater, whose own client plays it. A guest's
server turn played it into the broadcast to all, so the eater (every
single-player player, on the server thread) heard each bite twice: its
client's and the server's.
The server now sends a guest's Player.playSound to the others only, and
the client plays the last bite itself at USE_ITEM_COMPLETE
(completeUsingItem's emitParticlesAndSounds(16)), which it drew the
crumbs of but left silent. lan_e2e's eat check counts the bites heard
against the crumb bursts drawn.
Fixes#3129
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MushroomCow.addAdditionalSaveData / readAdditionalSaveData were never
ported, so a brown mooshroom (summoned, or turned by lightning) loaded
back red. MushroomCow.DATA_TYPE is entry 18 (after AgeableMob's
DATA_BABY_ID and AGE_LOCKED), not 17: on a vanilla server the colour
change from MushroomCow.thunderHit never reached the renderer, and a
Horizon host sent an INT where vanilla clients expect AGE_LOCKED's
BOOLEAN.
Fixes#3236
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Parrot.createNavigation and Parrot.calculateFlapping had no port, so a
parrot walked and hopped like a land mob, fell like a stone off a block
and never flew, tame or not.
- Parrot(): FlyingMoveControl(10, false), whose idle tick lets gravity
back on; the FIRE_IN_NEIGHBOR, FIRE and COCOA maluses at -1; and
createNavigation's FlyingPathNavigation (no doors, floats).
- ParrotWanderGoal: in water, LandRandomPos(15, 15); otherwise (all but
one time in a thousand) getTreePos, the first empty block with an
empty one above over leaves or a log within 3/6/3 in betweenClosed
order; else WaterAvoidingRandomFlyingGoal.getPosition, now shared as
Mob::flying_stroll_pos.
- calculateFlapping after aiStep: the wing state (folded on the ground
or as a passenger), and a fall through the air slowed to 0.6. A
multiplayer client runs the wing part for the parrots it displays.
- Parrot.isFlapping/onFlap from Entity.processFlappingMovement: the
entity.parrot.fly beat each half flapSpeed flown.
- canFlyToOwner: a parrot may teleport onto leaves next to its owner.
tools/test_mob_ai_parity.py gets four parrot scenarios (two on open
grass, one by a tree, one let go ten blocks up), on Peaceful because
Parrot.getAmbient's imitation roll uses the level's random. They match
vanilla on every tick; on main they diverge at tick 1.
Fixes#3091
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The check looked up `minecraft:mining_fatigue`, but the player's effects are kept by their bare name, so it never matched: the curse, its sound and the apparition came back at every 1200-tick pulse. As MobEffectUtil.addEffectToPlayersAround does, a curse as strong that doesn't end within the next minute now stays.
WitherBoss.hurtServer rejects damage caused by a non-player mob in
#wither_friends (the undead, withers included). Horizon only filtered
the wither's own targeting, so skulls and explosions from one wither hurt
its neighbours, which then retaliated through HurtByTarget.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
sky.rs draws the night look's backdrop: the sky in texel bands, stars that
twinkle in steps, three layers of hand-drawn clouds drifting a texel at a
time, a sun cut in slices and two skylines. Sprites are baked once per
appearance and scale into tiles that don't depend on the window's size; a
frame is asked for only when something on screen changes, never under
reduced motion (macOS, or HORIZON_LAUNCHER_MOTION=off), and not 10 s after
the window loses focus. It replaces the stone texture and Home's landscape.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub applied a player's move before the game ran the UseItemOn sent ahead of it, so BlockItem.canPlace checked the box after the move: mid-jump the server placed a block the client had refused, and only the client plays the place sound. The move now waits for the next tick, as it already does behind a dig. Fixes#3568.
MinecartTNT has no interact of its own, so Entity.interact passes and Minecraft.startUseItem goes on to the item; the server client ended every click on a minecart there. A command block minecart without a game master, and a rideable one when sneaking or ridden, pass the same way. Fixes#3588.
ComposterBlock.useItemOn and BoneMealItem.useOn are SUCCESS on vanilla's client, so Minecraft.startUseItem never tries the off hand; the server client predicted both as passing. A sapling, azalea or mushroom still passes there, as isValidBonemealTarget needs a ServerLevel. Fixes#3536.
Send the stored custom-name component through the server inventory codec and use vanilla float-to-channel conversion for RGBA shadow colors. Cover both paths with focused tests.
Co-Authored-By: Codex <[email protected]>
Draws the night tokens' type and colours in the launcher itself, over the
sky's stops, with each text colour's contrast on a plate over the brightest
stop and the rounded and pixel-cut plate corners side by side. Not a page
players reach; for checking the tokens in the real renderer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_theme::night: the night sky look's colours (NIGHT, and DAWN for Light),
type steps, radii and the Bricolage Grotesque cuts (tools/bake_bricolage.py,
OFL), with AA tests over every sky stop. tokens.css gains --night-*. The
launcher reads them: Night::blocks() dresses the screens not rebuilt yet in
the night's colours, and Bricolage replaces Geist and Pixelify Sans there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes missing item break sound effect and broken statistic tracking:
- When a tool breaks from mining a block (mine_block_with_held), route
durability wear through damage_held rather than clearing the slot silently.
- Route weapon wear in mace.rs and brush wear on armadillos in creatures.rs
through damage_held.
- Play entity.item.break sound and re-apply equipment when worn armor
breaks from Thorns in enchant_effects.rs.
Cod, salmon and tropical fish had only panic, player avoidance and random
swimming. AbstractSchoolingFish adds FollowFlockLeaderGoal (priority 5, no
flags): after a 100-110 tick start delay a fish joins a nearby school with
room or gathers the free fish around it, and followers path to their
leader while it lives and is within 11 blocks. FishSwimGoal stops while the
fish follows (canRandomSwim), a leader forgets an empty school on a 1/200
level-random roll, and schools hold 8 (5 for salmon).
Recruiting changes other fish, so it goes through two MobEvents that
apply_brain_events applies after the recruiter's tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
`complete_assets` also runs when Play is pressed, so a store with most of its objects
missing made the player wait for thousands of downloads and a sign-in with nothing on
screen. `refill_own_store` now runs in `reuse_prepared_everywhere`, on the start-up
thread, and Play only copies what is there; the gate's test pins both places.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
MultiPlayerGameMode.startDestroyBlock refuses before predicting when
Player.blockActionRestricted holds: an adventure player without a
can_break item. The client predicted the break anyway (sound, crack,
the block gone) and the server sent it back, and continueAttack swung
every tick. A stack the server sent with can_break still breaks; the
server judges its predicate.
Fixes#3027.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A world's data packs now change what the game plays with, the way
`ReloadableServerResources` loads a server's: recipes, loot tables,
advancements and tags from `<world>/datapacks` merge over the resource
root's own, at world open and on `/reload`. A pack's recipe or
advancement replaces the game's file for the same id
(`SimpleJsonResourceReloadListener` hands out the top resource of each
stack); tag files join the game's entries with `replace` dropping the
lower sources (`TagLoader.load`). Worlds without packs keep loading
exactly as before.
`datapack::DataStore` collects the packs' `recipe/`, `loot_table/`,
`advancement/` and `tags/…/` files (`DataStore.files`, the built-in
pack excluded: its files are the resource root's own, which the tables
read from disk). `horizon_world::loot::PackOverlay` carries them into
the loaders: `Items` (loot tables, item/block/entity_type tags),
`Recipes`, `StationData` (smithing, stonecutting, brewing, the recipe
book index), `advancements::Tree` and `LootData` (structure-block
containers). The game keeps the pack tables in `Game::world_tables`
and reads them through `Game::items`/`recipes`/`stations` (the
`world_items!`-family macros give the same field-level borrows at
conflict sites), rebuilding them when the world opens and on
`/reload`, with the advancement tree and the loot and LAN sync caches
forgotten alongside.
Not covered yet: worldgen registries, trade/banner/enchantment
registry files from packs, and the prepare-time behavior bundles
(mining facts stay with `block-behaviours.json`).
Co-Authored-By: ZCode Agent <[email protected]>
Ports CustomHeadLayer.submit for players and humanoid mobs:
- Renders carved pumpkins, jack o'lanterns, and mob/player heads worn
in the head equipment slot on the player model and humanoid mobs.
- Adds head_item_geometry to mob_block_geometry, player_model_geometry
(third person), and draw_player_preview (inventory preview).
- Correctly skips armor helmets (handled by HumanoidArmorLayer) and
body-slot species (equines, wolves, etc.).
- Keeps player model extra.head and extra.armor synchronized in
tick_player_model.
- Updates tools/port_map/client-005.tsv mapping.
streaming.rs moved to horizon-client-render (#3186) and main now sweeps far
columns only on some updates (if sweep); this branch's changes (no area at a
negative radius, rank_full_task, its test) are applied there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's handleUseItem sets the guest's known movement (ServerPlayer.getKnownMovement)
before the item use; this branch's horn sound is kept in the same closure.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractWindCharge is pickable, so ThrownEnderpearl's
ProjectileUtil.getEntityHitResult can strike it and onHit teleports.
Horizon only targeted mobs and players, so a pearl passed through a
charge. Add the charge's entity box to the pearl's targets.
A painting item with a `painting/variant` component (every one but the
first in the creative tab) placed a random painting and showed only
"Painting". HangingEntityItem.useOn runs Painting.create's random pick,
then EntityType.createDefaultStackConfig applies the stack's components
(Painting.applyImplicitComponents sets the variant) and the painting must
still survive. HangingEntityItem.appendHoverText adds the variant's title,
author and size, or "Random variant" in a creative tab without one.
The component also crosses the wire now (a painting_variant holder,
component 109): a creative client's pick reaches the server, and the
server's stacks keep their variant on the client.
Fixes#3503
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The player's armour layers (local player, other players, LAN guests)
never carried the glint, and the elytra drew with overlay 0. The armour
glint's overlay code, 13, was also the boat water mask's, so a glinting
layer drew depth only: an armour stand's enchanted chestplate was
invisible.
EquipmentLayerRenderer.renderLayers: each layer and its trim take the
armour glint when ItemStack.hasFoil, never the hurt tint (NO_OVERLAY).
ARMOR_GLINT moves to 35, past every material overlay of the entity
batch.
Fixes#3024.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A wither's block breaking (WitherBoss.customServerAiStep) calls
Level.destroyBlock, which sets the cell to fluidState.createLegacyBlock()
with flag 3. Horizon's mob BreakBlock event wrote air instead, without
neighbour updates: a wither in water carved an air pocket, a waterlogged
block lost its water, and the pool did not flow back in. The event now
sets the fluid's block through setBlock (skipped when that is already
the cell), and the wither's break sound (level event 1022) only counts
cells that changed, as destroyBlock's return value does.
Checked by a new "wither breaks" group in the command execution harness.
Fixes#3153
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game's side of the Launch API, experimental, version 0.1.0:
--game-dir (vanilla's --gameDir), --home, --fullscreen (vanilla's, not
saved to options.txt), --session-stdin and --launch-api, which prints
the API version and the options a launcher may pass as JSON.
A refused command line now exits 2 without a crash report; errors keep
1 and panics 101. The crash report's path stays the last stderr line,
its prefix now a constant (crash::SAVED_LINE).
docs/api/launch.md is the surface page; docs/api/launch-reference.md is
generated from game/launch_api.rs and a test keeps the two together.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The registry keeps its WebAssembly mods (Mod Menu, lookat, blockscope…)
in the index, so the modpacks that need them keep installing. The browser
neither lists nor installs one: its versions say "needs a native port".
A modpack still installs it, with its code left out (the game doesn't run
it), and Installed shows "Not ported: its code (needs a native port…)".
A WebAssembly mod is a `.hmod` that may do more than add content; content
mods are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game no longer runs WebAssembly code mods (the previous commit), so the
workspace drops what made and ran them: horizon-mods' runtime, commands and
test harness (wasmtime, wit-component, wit-parser), the SDK and its WIT
contract and examples, the sandbox's guest crates, and mods/official (the
official mods are native now, one native/<name>/ folder each in horizon-rust/horizon-mods).
horizon-mods keeps content mods, modpacks and the registry index (whose
api_level still reads published .hmod files). `cargo horizon-mod` makes
content mods only: `new` has one template, `build` refuses a crate,
`check` refuses a .hmod with code and points to native mods, and `test`
(the sandbox) is gone. CI no longer installs the wasm32 target.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/mods.md is now about content mods (`cargo horizon-mod`, the manifest, the
content fence), native mods, publishing, the official native mods and a short
note on the removed WebAssembly mods. docs/api/mods.md and mods-reference.md
(the code API) are deleted. Other pages, the CLAUDE.md crate table and the
roadmap (five WebAssembly API items removed, others reworded for native mods)
no longer describe the sandbox, the SDK or the WIT.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A download from Mojang that stopped on the way (the assets' failure is one log line)
leaves the asset store with some of its files, and every completion that copies from
it (the sounds, the languages, #1870) then finds nothing to copy and the game plays
silent and in English. Asking for it again did not help: the folder is found as it is,
so Prepare again never reached Mojang.
`complete_assets` now asks the owner gate again (`download_for_owner`, the one door to
Mojang's files) when Horizon's own store exists and a prepared version has no sounds or
no languages, once per run. The download keeps the files already whole, checked by sha1,
and fetches the rest; a non-owner fetches nothing, which the gate's test now also covers
for a store that lacks assets.
In that download each asset is tried three times and one that keeps failing does not
stop the rest: the log says how many failed and the first one's reason, and the next
run fetches what is left.
Requests had no timeouts, so one connection that went quiet held the whole download
for ever: the agent now gives up after 15 s to connect and 60 s without data.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The options pages, the mod and shader pack lists, the agent button and the
multiplayer friend screens drew their own labels as literals, so a client with
ru_ru stayed English there. They read `horizon.*` keys now, and the bundled
namespace carries both languages for every one of them.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Player.attack ends before hurtOrSimulate and causeExtraKnockback when
neither the scaled ATTACK_DAMAGE (clamped to [0, 2048], so Weakness can't
push it below 0) nor the scaled enchantment damage is left. The remote
path cut the attacker's speed on knockback alone, so an empty-hand sprint
hit under Weakness I still slowed the client.
The decision moves to attack_strength::attacker_slows_after_hit, with the
knockback formula shared with the local path as
attack_strength::extra_knockback, and unit tests for a charged and an
uncharged sprint hit, a knockback weapon, no sprint, a no-damage hit
under Weakness, enchantment damage alone, and a server-raised attack
speed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
MultiPlayerGameMode.attack runs Player.attack on the client too. For a
mob the client's hurtOrSimulate fails, so the hit has no attacker side,
but RemotePlayer.hurtClient is true: a sprinting (or knockback-enchanted)
hit on another player runs causeExtraKnockback's attacker side on the
client, cutting horizontal speed to 0.6 and stopping the sprint. The
remote attack path only sent the packet, so after such a hit the client
kept its speed while the server's player slowed, and the server pulled
the player back (seen on Hypixel when sprint-hitting a nearby player).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
The first perf_bench run on Windows: Horizon v0.2.97, vanilla 26.3 and vanilla with Sodium,
Lithium, FerriteCore and ImmediatelyFast, render distances 8, 16 and 32 at 1280x720, three
games each. The reports are in benchmarks/perf. The page next to them gives the setup, the CPU
load and compiler activity during each run (Windows has no load average), and the tables.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Projectile.shootFromRotation adds source.getKnownMovement() to the
projectile, the vertical part only when the shooter is off the ground. For
a ServerPlayer that is lastKnownClientMovement: the move the client last
sent (handlePlayerKnownMovement), reset to zero by handleClientTickEnd when
no move came that tick.
Horizon added the player's PlayerMotion velocity instead. On the server
(single player's server thread, LAN, dedicated) a guest's velocity only
holds knockback, not the player's own movement, so an arrow shot while
jumping or running lost the shooter's motion and flew lower and shorter
than vanilla's.
Guests now keep known_movement: set on each accepted move, zeroed at a
ClientTickEnd without one. enter_guest puts it in Game::known_movement for
the turn and leave_guest restores the outer turn's, so nested turns keep
their own. Bows, tridents, snowballs/eggs/ender pearls, thrown potions
and wind charges take known_player_movement(), which the spear already
used. The spear and the per-tick using/digging turn no longer copy the
last of the tick's moves themselves, which was zero once those moves had
been consumed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
with_packs replaces a built-in biome in place and adds new ones after the
built-in ids, which never move; registry_order gives vanilla's network
order (Identifier: path, then namespace, as RegistryDataLoader sorts).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/fixtures/tall_datapacks/horizon_tall_test is Horizon's own pack
(MIT): an Overworld dimension type of min_y -64, height 2096 (To The
Max's, the highest top vanilla allows) and noise settings that raise
terrain to about y 1900. VanillaChunkGenerationOracle builds each level
at its dimension type's range instead of a fixed -64..320, so
HORIZON_PARITY_DATAPACKS=tools/fixtures/tall_datapacks
tools/test_datapack_worldgen_parity.py compares Horizon's tall generation
with vanilla's: 16 chunks over two seeds, terrain and decorated, the same.
docs/world-height.md describes the model (one LevelShape per level, from
its dimension type, read everywhere), the network path, the checks and
what is left; world-generation.md and the README say packs' dimension
types now apply.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sections of one block, one biome and one light value are one allocation
per kind, shared by every column (new chunks, lighting, saved light);
edits copy them, as Arc::make_mut already does. A tall level is mostly
such sections: air with full sky light above the terrain. The relighter's lowest_sky_source passes
over a uniform section that light goes through instead of testing each
of its 16 blocks.
Memory only: every value read is the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Opening a world reads its levels' shapes from its dimension types, the
enabled data packs' over vanilla's (world_loading::level_shapes), and the
game keeps them (Game.level_shapes): each level's World, SaveDir,
streaming and flat terrain get its shape, and the teleport and portal
searches read a level's logical height from them. A host encodes each
level at its shape (HubConfig.levels) and sends the data of a dimension
type the packs changed even to a client that knows the vanilla pack
(NetConfig.levels, SyncData::registry_packets), so the integrated server
of single player and LAN guests see the same heights. A remote client
takes each level's shape and its chunks' column from the server's
dimension type, which now carries its logical height; the vanilla End's
fallback has sky light, as 26.3's the_end.json does.
A world with To The Max's dimension type is 2032 blocks high from here.
Vanilla worlds read vanilla's types: nothing changes there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
About ninety places took "where the level starts and ends" from the fixed
column: void damage, build limits, spawn and surface searches, falling
blocks, wind charges, commands, mob goals, navigation and paths, raids,
villagers' POI scans, fluid scans and the portal search. Those that used
the column for every dimension read the world's (World::bottom_y, top_y),
which is -64..320 in all three vanilla levels; those that chose per
dimension read the level's shape (LevelAccess from world.shape, Dim's
bounds from Dim::shape). Each level's World now holds its shape where it
is made. chunk::every_section replaces the per-file [true; SECTIONS]
arrays of the fluid scans.
Every value read is the one in use today.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's dimension_type/<id>.json joins the overrides (DIMENSION_TYPES).
LevelHeight is a dimension type's build range, logical height and
skylight, checked as its codec checks them; WorldgenOverrides::level_height
gives a vanilla level's, from the type its level stem names. The
generator takes the level's range from the same data instead of the
fixed table (dimension_type_height goes), so a pack's taller dimension
type no longer clamps the noise, and datapack::worldgen_overrides no
longer reports dimension types as left out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The random-tick section cache keeps a section list as long as the chunk's
and counts its Y from the chunk's min_y. A generated chunk becomes a
column of its level's (OverworldGenerator::level gives the build range,
ColumnLayout::of_level its column), copying the whole level range, as
does a failed one. Distant terrain from proto chunks spans the level's
range, the minimap and saved-chunk far tiles scan from the chunk's own
bottom, and flat worlds lay their layers from the Overworld's minimum up
to its height (FlatTerrain::from_settings takes the level's shape). A
test ticks the sections of a 131-section chunk near its top.
Vanilla's levels keep their column and ranges, so nothing changes there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The LAN encoder encodes a level of a LevelShape: its chunks hold the
shape's column, of which the level's own sections go on the wire, and the
heightmaps it sends count the column's sections from the chunk's min_y
(client_heightmaps_in takes it). A section's block entities sit at the
chunk's min_y. The remote client decodes chunk and light packets into the
column its ChunkFormat holds (the vanilla one until the server's
dimension types set it) instead of the fixed -64..320. A test encodes a
131-section column and checks its sections and light layers all go out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SaveDir::new takes the level's LevelShape from its caller instead of
guessing it from the folder name (LevelShape::for_dir goes). A saved chunk
is decoded into its level's column (decode_with, decode_proto), the
sections outside it left out; encoding, POI sections, heightmaps and the
saved light layers count sections from the chunk's own min_y, and distant
terrain read from saves spans the level's column. A test saves a
131-section chunk (yPos -4, 12-bit heightmaps) and reads it back whole,
and into the vanilla column without the sections above it.
Vanilla levels pass their vanilla shapes, so saves are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
World holds its level's LevelShape, and its chunks' column is the shape's.
Writes check the chunk's own range, the heightmap fallbacks and may_hold
read the column, and dirty section indexes are bounded by the mask instead
of the vanilla constant. Streaming marks the column's sections dirty, the
mesher places sections from the chunk's min_y, and the renderer builds its
occlusion graph, complete-column mask and section origins for the column
of the level it was cleared for (clear_sections takes it). A test streams
a 131-section level (To The Max's height) and checks that columns light
and mesh to y 2031, and that an edit at y 1800 relights and remeshes.
Every level is still the vanilla Overworld's shape here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
light_chunk floods up to the centre chunk's own top, and the light it
returns, the saved and received light layers and the relighter's section
keys span the chunk's sections instead of the vanilla constants. The
relighter takes the column from the world it lights (LightWorld::layout,
the column its chunks hold), and LightChanges counts section indexes in
that column, so a taller level's sections near its top get relit and
remeshed too. Vanilla's column is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LevelShape (skylight, min_y, height, and now logical_height) moves from
save.rs to chunk.rs, where every subsystem can read it, and checks a
dimension type as its codec does. Its column is the span of sections a
chunk of the level holds: the level's own range widened to the vanilla
Overworld's -64..320, so vanilla's three levels keep the 24 sections
they have always had. ColumnLayout is that span
as a value; Chunk records its own min_y, and get, set, highest, light,
biome and the heightmap cache read the chunk's sections instead of the
constants.
Nothing makes a chunk of another column yet: vanilla is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dirty sections, the light edit log, random-tick and portal section caches,
the renderer's meshed-column masks and the LAN encoder's light masks held a
bit per section in a u32 or u64, which capped a column at 32 sections. A
256-bit SectionMask holds every layout a dimension type allows (254 sections
plus the two light borders). No behaviour change: the column is still 24
sections.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
session_lock_is_exclusive read session.lock while its lock was held.
Windows file locks are mandatory, so that read failed with OS error 33.
It now reads the file once the first lock is dropped.
port_publish wrote the store path as str(path) with the home replaced by
"~", which kept Windows backslashes (~\code\...). The path under the home
is now written as ~/ plus its POSIX form, as on Linux. The temporary body
file is opened as UTF-8, so the "→" in a facts line no longer fails to
encode in the Windows code page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A finished piston's block is drawn over the chunk mesh until that mesh
shows it, but it was lit with the light stored at the moving piston's own
cell. Once the placed block stands there and the edit is relit, that cell
is opaque and holds 0: the pushed or pulled block and the piston base
flashed black for the frames until the mesh came up (#3002).
Vanilla's PistonHeadRenderer.createMovingBlock gives each block a
MovingBlockRenderState (the lone block at the cell it moves from, the
level's light engine), and MovingBlockFeatureRenderer tesselates it with
ModelBlockRenderer.tesselateBlock: in tesselateFlat each quad reads
BlockModelLighter.getLightCoords at its cull-face neighbour, or
prepareQuadFlat's position (the neighbour when cubic, else the block).
Moving blocks now follow that through MeshTables::flat_light, the chunk
mesher's own flat-light rules; a finished one is lit as the block placed
at its cell, which it stands in for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PistonMovingBlockEntity.saveAdditional stores facing through
Direction.LEGACY_ID_CODEC, Codec.BYTE of get3DDataValue. The block-entity
canonicaliser rewrote it as an int too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A stew taken from the creative inventory gave no effect when eaten and
its tooltip listed none (#3137). The protocol read the
`suspicious_stew_effects` component past without decoding it, so the
creative client's `SetCreativeModeSlot` sent the server a bare stew and
the server's copy had nothing for `SuspiciousStewEffects.onConsume` to
apply. Decode and encode it (`Entry.STREAM_CODEC`: mob effect id,
duration) both ways, and add `SuspiciousStewEffects.addToTooltip`'s
creative-only effect lines (`PotionContents.addPotionTooltip`).
Fixes#3137.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
- Moving blocks are tinted per face with the chunk mesher's block colours
(MeshTables::block_tints), not the terrain particle tint, which is none
for grass blocks: a pushed grass block was drawn gray.
- PistonMovingBlockEntity is saved with its chunk as vanilla's
minecraft:piston block entity and loaded back into the level's moving
pistons, so blocks caught mid-move finish it after a rejoin instead of
leaving an invisible, solid moving_piston (#3310).
- A server client drops its moving piston when the server changes that
block (LevelChunk.setBlockState removes the block entity), instead of
animating it for five more death ticks over the placed block, black and
z-fighting (#3002).
- A finished piston stays drawn until the chunk mesh shows its block, and
no longer, so no frame shows neither.
Fixes#3310Fixes#3002
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- docs/datapack-upgrade.md: how to run `horizon-port-pack
upgrade-datapack`, every rewrite and how it is checked, and what is not
rewritten; linked from docs/README.md and the command's help.
- tools/datapack_upgrade_rosetta.py writes vanilla 26.2's own worldgen out
as a pack, upgrades it and compares it with vanilla 26.3's file by file:
registries whose format changed and whose content did not must come out
identical.
- tools/datapack_upgrade_fidelity.py starts vanilla's 26.2 and 26.3
dedicated servers on the same seed, the first with a pack and the second
with its upgrade, force loads the same chunks and compares the saved
blocks; without packs it measures what 26.3 itself changed.
Both need the 26.2 server jar the player downloaded; nothing is fetched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fourth part of the 26.2 to 26.3 data pack upgrade.
- Configured features move to `feature/` with `config` unwrapped, block
state providers take their 26.3 names (a `simple_state_provider` is its
state), and nested and inline placed features are upgraded with them.
Feature types 26.3 replaced by compositions of other features are listed
in the report and left as they are.
- Placement `random_offset` becomes `offset` with one provider per axis
(x and z both from `xz_spread`).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The third part of the 26.2 to 26.3 data pack upgrade: noise settings in
26.3's form, with the parts 26.2 hard-coded written out as data.
- The beardifier, which 26.2's NoiseChunk added, is added to the final
density.
- The aquifer router fields move to `aquifers`, whose `exclusion` spells
out 26.2's `isDeepDarkRegion` (erosion < -0.225 and depth > 0.9) from
the pack's own erosion and depth; `surface_level` is the preliminary
surface, and `chunk_surface_level` its per-chunk 16 x 1 interpolation.
- 26.2's ore veinifier becomes two `ore_vein` material rules (copper,
iron) with OreVeinifier's heights, thresholds, edge roundoff, solidness,
richness and gap, ahead of the surface rule: 26.2 placed veins while
filling the noise and its surface pass only replaced the default block.
The rule is written as a named `material_rule` under the settings' id.
- The climate functions are named, and the spawn target refers to them by
id (`depth` and `offset` dropped, as vanilla did).
- `size_horizontal`/`size_vertical` are dropped: each `interpolated` holds
its cell.
- Inline noise settings in dimensions and world presets get the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The second part of the 26.2 to 26.3 data pack upgrade: named density
functions in 26.3's form.
- Inputs are named `input`/`left`/`right`; `invert` is `reciprocal`,
`y_clamped_gradient` a `gradient` on y, `shifted_noise` a `noise` with
shifts, `end_islands` `end_outer_islands`, and `constant` its number.
- 26.2's `flat_cache` held each column's value at y = 0, so it becomes a
`cache` around a `slice` at y = 0 when its input depends on y (through
the pack's own functions); 26.2's other caches only memoise in 26.3.
- `interpolated` takes its cell from the noise settings whose router
reaches it (`size_horizontal * 4`, `size_vertical * 4`), which 26.2 read
from the settings, or vanilla's overworld cell when none does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 refuses a world generation data pack written for 26.2: the
registries changed format. `horizon-port-pack upgrade-datapack <pack>
(<out> | --world <world>)` reads a pack (zip or folder) and writes it in
26.3's formats, with a report of what it rewrote, what has no faithful 26.3
form and which types it does not know. Each rewrite is the one vanilla's own
data went through between 26.2 and 26.3.
This first part handles the registries that need no context:
- noise parameters: `firstOctave`/`amplitudes` become `base_octave`,
`octave_count`, `amplitude_modifiers` and the base amplitude of
`NormalNoise.createParity`, which keeps 26.2's normalisation;
- block states: `{Name, Properties}` become `{id, properties}`, or the id;
- configured carvers move to `carver/` with `config` unwrapped and the cave
carvers' hard-coded count, thickness and room scale written out;
- biome spawners and spawn probability become 26.3's gameplay attributes;
- structure spawn overrides and the dimension type's bed rule.
Density functions, noise settings and features follow in their own
changes. serde_json gains `float_roundtrip` so the numbers the upgrade
writes back are the ones it read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The surface stage's per-chunk cell cache only proved cells whose eight
quart corners hold one biome; a block in any other cell ran the full
eight-corner zoom mix and a stored lookup again. Now a mixed cell keeps
its corners' jitter (BiomeZoom::corner_jitter, one mix per cell) and
their eight biomes, so each block of the cell only redoes the distance
comparison (BiomeZoom::select_corner, same arithmetic and tie order); the
cell of the previous lookup is kept too, as columns walk down in y. A
test checks the split zoom against quart_at_block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every solid block of every column asks the rule tree's biome conditions
for its biome. Each one cloned and dropped the biome's Arc<str> (an atomic
count all generator threads share) and looked its name up in a string set.
Queries that number their biomes (the generator's) now answer with the
index, and biome conditions keep a bit per index; others compare names as
before. Same answers: the generator's name -> id map is exact.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the original custom-name component as the sole stored value, deriving plain text for gameplay and rendering its styles in tooltips, held labels, and item links. Honor shadow_color in parsed text and HUD glyph passes. Extend the Java/Rust tooltip parity fixture with styled names; its fixture format builds on PR #1702 by RVZO6.
Co-Authored-By: Codex <[email protected]>
Give every vanilla class its Rust home files, weighed from all its matches.
In those files, match a method to the fn that touches the same fields and
reaches the ports of the same callees. Read tools/port_map/*.tsv, where a
person or agent records a verdict for what nothing else finds (port, inline,
missing, skip with a reason), and list the next unanswered batch with todo.
Count field-only constructors as trivial and fold initializer lambdas into
<clinit> and <init>.
Read the game jar's bytecode for exact call targets, constants and ids, and
resolve Rust calls through the receiver's type. Match more methods: same-named
fns in an impl or file named for the class (the impl type must fit the class),
and callees of known pairs linked through the call graph. Leave trivial methods
out of coverage.
New gap kind drift: a port that never names a sound, particle, item, block,
effect or game event its vanilla method uses, or lacks one of its rare numbers.
Seam gaps on the old in-frame path sort last; maths and geometry callees no
longer make seam or callee gaps.
New commands: issue (the classes, Rust code and gaps a bug report points at),
check (what a branch touches, gaps it opens and closes against its base), diff,
and a searchable offline report.html.
Include sulfur cubes in AbstractCubeMob size metadata serialization and
remote data handling so adult cubes retain their size across the network.
Adult sulfur cubes spawned from spawn eggs now correctly render and act
as adults rather than defaulting to size 1 babies.
Vanilla reference: AbstractCubeMob.ID_SIZE, SulfurCube.setSpawnSize.
Fixes#3151
F2 kept only the SDR frame. With HDR on it now also writes `…-hdr.png` beside
vanilla's screenshot: the HDR pass drawn once more into a 10-bit PQ target of its
own (so scRGB swapchains get one too), read back and saved as a 16-bit PNG with
the cICP chunk of PNG's third edition (BT.2020, PQ, full range) and cLLI (peak,
paper white). Each 10-bit value keeps its top bits below it, so 0 stays 0 and
1023 becomes 65535. Vanilla's screenshot is written as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/port_ledger.py indexes the decompiled sources and the Rust tree into .local/port-ledger/ledger.json and answers where/what/class/coverage queries both ways, with the gaps where a port isn't wired like vanilla (seam, client, class, callee, stale).
Cancel item-model centering in the vanilla carried-block pose. Add a regression test using point coordinates captured from Minecraft 26.3 CarriedBlockLayer.submit, including a transformed entity root.
Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV tested the updated Linux binary in game and confirmed the fix works.
Co-Authored-By: OpenAI Codex <[email protected]>
Render the carried block through the inventory item model pipeline so every quad uses its proper texture and tint source. Remove the single-sprite cube renderer and let the item pipeline center the model.
Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV confirmed the fix works in game.
Co-Authored-By: OpenAI Codex <[email protected]>
Only a soul lantern needed it, and it lifted less for its light level, not for
its texture: that is better fixed where the lift is worked out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bright tier's window goes to 0.04-0.28, from a round in the HDR gallery: a soul
lantern's glass (light level 10) now lifts, and the level-15 bright blocks still look
right.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Very bright's window (0.88 to 0.94) was tuned on the sun alone: on a block
only the hottest pixels reached it, and they blew out while the rest
stayed below bright. It now lifts over bright's window with the whole
headroom; the sun keeps its yellow corona near the horizon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Right-clicking a water bottle on mud-convertible blocks started a drink, only dispensers swapped. Vanilla (PotionItem.useOn) pours it out onto any side but the underside and sets mud.
The aim ray skips water, so these items could not be placed on the surface. Vanilla (PlaceOnWaterBlockItem.use) uses a source-only fluid ray and places on the position above the block it meets.
main's dark disc and void fog read WorldGen.flat, which is now the preset
(seen by sirrayi's review).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client only had an arm for text displays. DisplayRenderer draws a block display's model or an item display's item under the same billboard and transformation, with the brightness override or the light at the entity.
Summoned displays reached clients as bare entities, so a text display showed nothing. Vanilla syncs Display's entries 8 to 22 and each type's own (text 23 to 27, block state 23, item and context 23 and 24) from the saved compound.
Adventure players could never place or break. Vanilla (Player.blockActionRestricted, ItemStack.useOn) allows it when the held item's can_break matches the block, or can_place_on matches the clicked one.
A strike hurt mobs as generic damage, which bypasses armour and wolf armour and loses the is_lightning loot condition (a struck turtle dropped no bowl). Vanilla (Entity.thunderHit) uses lightning_bolt.
The spawner existed only on the server, so a client never saw the item grow and turn. Vanilla tracks it with DATA_ITEM; the client draws OminousItemSpawnerRenderer and runs tickClient's particles.
The cramming roll was drawn but nothing hurt the mob. Vanilla (LivingEntity.pushEntities) deals 6 cramming damage, 1 in 4 ticks, above max_entity_cramming - 1 pushers who are not riding.
A server sent every arrow as a plain one. Vanilla sends a spectral arrow as its own type and a tipped arrow's ID_EFFECT_COLOR, and the client draws arrow_spectral.png and sheds the effect swirls (Arrow.tick) and sparkles (SpectralArrow.tick).
Arrows built from a bow or crossbow carried no effect and mobs never got one. Vanilla (Arrow, SpectralArrow doPostHurtEffects) applies the pickup item's potion contents at its duration scale, or Glowing for 200 ticks.
A stack's potion_contents lost custom_effects and custom_color on load, and drinking used scale 1. Vanilla (PotionContents.onConsume) applies the potion and custom effects at potion_duration_scale.
A `/forceload`ed chunk holds a FORCED ticket at level 31, and vanilla's
level rises one per ring, so the chunk itself and the two rings around it
are level chunks with their light, the third ring has its features and the
fourth only its terrain. Horizon took the forced chunk's reach from the
caller's margin instead, so only the chunk itself ever finished and its
neighbours stayed proto chunks.
The reach now follows the ticket levels: the full ring is two chunks out,
the features ring three and the terrain ring four, and the protos of the
terrain ring are kept so the features ring can be decorated from them. A
decorated proto chunk is saved as `minecraft:initialize_light`, the status
vanilla gives a chunk that has its features but no light.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The "Thanks to ..." line read as part of the section before it (often Known issues), on GitHub and on playhorizon.dev/releases. It now has its own heading, and a horizontal rule sets the disclaimer apart. Fixes#2314.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`DamageSource.getLocalizedDeathMessage` uses the `<id>.item` message when
the cause holds an item with a custom name, with the item's name as a
third argument: "Steve was shot by Zombie using My Sword". Horizon always
used the two-argument message, so the item was never named.
The mob's main hand item now keeps its custom name, and the death message
picks the message and its arguments from it.
Fixes#2802
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The traveller was dropped from the queue before the exit was resolved, so a
failure to resolve it lost the entity for good. The traveller now waits and
the failure is retried, with the reason in the log.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The script plays the End in a scratch home: it places a gateway beside the
spawn platform, puts a cow and a seven-diamond stack in it, and checks that
both leave it and land at the gateway's exit with their data, in the
server-thread and the in-frame singleplayer.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Upstream now keeps a gateway's exit in the gateway's own block entity
(TheEndGatewayBlockEntity.setExitPosition), so the player travels to the
exit the gateway was given (#1517). The entity travel stays: the mobs and
the dropped items go through the same gateway, and the exit the first
traveller found is reused, so the player and the entities land in the same
place and only one island is built for a gateway.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The pull request's last push did not start a run; this empty commit
starts one on the same tree.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
main's Superflat Customize screen (#2214) still set the removed WorldOptions.flat: it now picks WorldPreset::Flat,
and the agent's customize-flat state reads the preset. structure_mobs.rs's test passes the new preset argument
(minecraft:normal) to VanillaTerrain::for_dimension.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The braces were one too many, so the file did not parse and the Python
tests failed on it. The command it builds is the same now: the item
stack {Item:{id:"minecraft:diamond",count:3}}.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
Only the player was checked for End gateways, so a cow or a dropped
stack standing in one stayed there for good. The entities now go
through a gateway the way the player does: the block their box swept
through takes them in (unless the gateway is cooling down, as
EndGatewayBlock.entityInside does), they travel on the next tick
(Entity.handlePortal, with the End gateway's transition time of 0) and
land where the gateway leads once its chunks have loaded, keeping their
data and counts.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.
The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
While nobody was online, the dedicated server behaved as if a player
stood at the world spawn: at startup it streamed a view-distance area
there and placed a stand-in player, and afterwards it kept that area
loaded, generated and ticked. Vanilla only loads the chunks its spawn
spiral tests (MinecraftServer.setInitialSpawn: getChunk(FULL), whose
3x3 is decorated in one generation task), has no spawn ticket, and finds
a player's spawn when one joins. In a pre-generated world the startup
area decorated in another order made ore blobs and a mineshaft start's
saved flags near spawn differ from vanilla's.
A streaming radius below 0 now means no area around the centre. The
dedicated server only finds the world spawn, loading each tested chunk
as an extra area of radius -1 ranked as one FULL task (x-outer,
z-inner), and keeps the radius at -1 while no guest is online.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Light level says how far a source lights its surroundings, not how bright
it looks: a candle's flame is as bright as a torch's. Light sources now
get a tier (mesher::hdr_tier): very bright (the sun), bright (glowstone,
campfires, the soul fires), incandescent (flames, lava), low (redstone
torch, magma, a candle's wick) or none (glow lichen). Unlisted ones go by
their level. The tier rides in the emission bits as the level did, and
each has its own share of the headroom and its own lift window
(gfx/hdr.rs TIERS), tuned by eye on an HDR1000 OLED.
Flames, lava drops and obsidian tears now shine too: their tier rides in
the particle vertex's overlay bits, and with HDR on a second draw of the
entities writes only the frame's alpha for them, so their colour still
blends as before.
HORIZON_HDR_DEBUG=1 now greys what isn't lifted and tints what is, so a
lift shows on purple and red light too.
Part of #1441.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The superflat screen and the agent's state read the world preset that
#2214 adds instead of the old flat flag; drop a stray parenthesis in
the horizon-prepare hint.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
The player's file now names the vehicle it rides, the way vanilla's
ServerPlayer.saveParentVehicle does, and the ride comes back on the next
login in both single player's modes.
- The player compound holds a "RootVehicle" compound whose "Attach" is the
UUID of the vehicle at the root of the rider chain (Entity.getRootVehicle).
Horizon keeps the vehicle in the world, so the file names it instead of
holding a second copy of it: the horse and its equipment stay in the
chunk's entity file, and nothing is duplicated.
- The single player's own player writes the vehicle it rides, and a server's
guests write theirs; on the next login the vehicle is looked up by its
UUID among the entities in the world, and the player rides it again.
- A guest whose file names a vehicle gets back on it once the vehicle is
loaded, as ServerPlayer.loadAndSpawnParentVehicle does.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
NaturalSpawner.spawnMobsForChunkGeneration runs in the generation
pipeline (spawnOriginalMobs), with the WorldGenRegion's random for the
spawn rules and finalizeSpawn. Horizon ran it from the game tick for
chunks that were fresh and lit, so:
- a world generated with ticks frozen, as a seed generator does, got no
animals at all, and a chunk saved before a tick saw it lost them;
- the rules and finalizeSpawn drew from other randoms, and the reduced
spawning::finalize gave no variants, babies or group data;
- the spawn list came from the biome's noise cell, not the zoomed biome
(fullResolutionBiomes) vanilla reads;
- members of a pack were kept apart, though a WorldGenRegion has no
entities in the way.
A chunk now gets its animals as soon as it is lit, whatever the tick
state. The positions come from the same selection, the rules and the
finalize from the chunk's region random, and each mob is made with its
constructor's state, finalized like a structure mob
(Reason::ChunkGeneration, group data per pack) and loaded from the NBT
generation would save. The natural spawn oracle's generation mode sees
no entities, as a WorldGenRegion.
Goats and frogs keep their long jump and ram cooldowns under vanilla's
memory names (long_jump_cooling_down, ram_cooldown_ticks), and rabbits
save the attack damage attribute Rabbit.setVariant makes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.
The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
ProtoChunk.addEntity saves a structure's entity as soon as it is placed,
and the chunk loads it again when it becomes full, so vanilla keeps what
each entity's own save writes. Horizon kept the template's NBT with its
model on top, and its model left out some fields:
- the keys in vanilla's templates that no entity reads (food_level,
lastRestock, CatType, OwnerUUID, ...) are dropped;
- villagers save VillagerDataFinalized and their age (AgeableMob writes
Age, ForcedAge and AgeLocked whenever canBeABaby, not only for
creatures; never for a frog, a parrot or a zombie nautilus), and the
age is read back for the same mobs (a baby villager or hoglin no
longer grows up on load);
- iron golems and the other neutral mobs save anger_end_time;
- zombies, drowned and piglins save the movement speed attribute that
their setBaby creates on load;
- a drowned with a nautilus shell keeps drop_chances (offhand 2.0), and
saved drop chances are read back;
- the rider of a new chicken jockey is saved as it was when the chicken
was added, before its equipment and attributes, as vanilla's chunk
saves the chicken with its passenger at that point.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A goat horn did nothing when used. It now plays its instrument at the
player, is held up for the instrument's duration, goes on cooldown, posts
the instrument_play game event and counts as used. Other players hear it,
see the arm raised to the head, and the tooltip names the call.
2026-10-26 19:54:15 +01:00
964 changed files with 104183 additions and 23818 deletions
| `horizon-anticheat` | The dedicated server's anticheat, off by default: `[anticheat]` settings, violation levels, the checks' rules; the hub runs them (`lan/anticheat.rs`) ([docs/anticheat.md](docs/anticheat.md)) |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and`cargohorizon-mod`(`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
@@ -71,7 +74,7 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client --features horizon-client/mods --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
@@ -91,9 +94,6 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
- What a change reruns (of 329 harnesses, measured 2026-10-23): a file of the game binary (`game.rs`, `game/`)
~25, a file of the client lib ~103, of `horizon-world` ~103–109, of `horizon-core` ~119. Plan for it.
@@ -46,9 +47,9 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates, item modifiers and world generation (vanilla's heights and biomes for now).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates, item modifiers and world generation, taller levels included (vanilla's biomes for now).
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining servers, online-mode ones with the Microsoft account the launcher signs in with. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mods, installed from the launcher. The official ones (Zoom, FPS Counter, Keystrokes and Minimap) are native Rust that the launcher builds into your game, with no Rust to install; older WebAssembly mods still run while they move. Java mod jars do not run in Horizon. See [Mods and data packs](https://playhorizon.dev/docs/mods).
- **Mods:** content mods add blocks, items and data, and native mods change the game's own Rust code, built by the launcher on your copy, with no Rust to install. Horizon's own (Zoom, FPS Counter, Keystrokes, Minimap, Recipes and Coordinates HUD) are native mods. Java mod jars do not run in Horizon. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
@@ -127,6 +128,8 @@ It runs rustfmt, clippy with warnings denied, the tests of the crates you touche
## Project status
**Player-visible gaps: 833 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Screens, HUD and client packets [197](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [129](https://github.com/horizon-rust/horizon/issues/3519); Animals [110](https://github.com/horizon-rust/horizon/issues/3512); Server and players on the server [106](https://github.com/horizon-rust/horizon/issues/3520); Blocks, redstone and fluids [86](https://github.com/horizon-rust/horizon/issues/3516); Entities, projectiles, vehicles and decorations [64](https://github.com/horizon-rust/horizon/issues/3514); Monsters and bosses [53](https://github.com/horizon-rust/horizon/issues/3513); Mob AI, villagers and raids [35](https://github.com/horizon-rust/horizon/issues/3521); World, world generation and saves [32](https://github.com/horizon-rust/horizon/issues/3517); Items, enchantments and inventories [21](https://github.com/horizon-rust/horizon/issues/3515). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
The table below is the project's own audit of each subsystem. Every subsystem now matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not proof that nothing else differs. The remaining known differences are listed below.
### Java parity: 100%
@@ -163,7 +166,7 @@ lists every method of vanilla 26.3 next to the Rust functions that port it, with
### Known issues
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
/// How long the trial run plays: two seconds of game.
constTICKS: u64=40;
constFRAMES_PER_TICK: u64=3;
pubstructReport{
pubkind: Kind,
pubfacts: Facts,
pubsha256: String,
pubsize: u64,
pubsigned: bool,
/// A mod has code to run (a content mod may have none).
pubcode: bool,
/// A mod's component imports.
pubimports: Vec<String>,
/// A mod's commands, each run once.
pubcommands: Vec<String>,
/// What the mod showed during the trial run.
puboutput: Vec<String>,
}
implReport{
@@ -53,10 +37,6 @@ impl Report {
"sha256": self.sha256,
"size": self.size,
"signed": self.signed,
"code": self.code,
"imports": self.imports,
"commands": self.commands,
"output": self.output,
})
}
@@ -100,18 +80,8 @@ impl Report {
ifself.kind==Kind::NativeMod{
out+=" native source: unsandboxed; not executed by the registry check. Compile and test against the exact pinned Horizon and Mixins revisions before review.\n";
description="The dedicated server's anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
Loaded 100 of 964 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.