100 Commits
Author SHA1 Message Date
d3grandoandClaude Opus 5.5 95d9303204 Ender pearls go through End gateways
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]>
2026-10-30 14:44:47 +07:00
d3grandoandClaude Opus 5.5 1e8f93a692 Check tame wolf kill credit in the command execution harness
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:39:25 +07:00
d3grandoandClaude Opus 5.5 034191391b Credit a tame wolf's kills to its owner
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]>
2026-10-30 14:39:25 +07:00
d3grandoandClaude Opus 5.5 d225a5fdf7 Merge origin/main into fix-3513-cube-hurts-golem
mobs.rs: this branch's MobEvent::CubeHurtMob and main's StabDismount, both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:38:42 +07:00
d3grandoandClaude Opus 5.5 e66a24edd4 lan_e2e: pet_death check for the owner's death message
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:31:26 +07:00
d3grandoandClaude Opus 5.5 df71f965b0 A tame pet's death tells its owner how it died
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]>
2026-10-30 14:31:26 +07:00
d3grandoandClaude Opus 5.5 912b671416 Volume sliders play a preview sound on the title screen's options
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]>
2026-10-30 14:22:23 +07:00
d3grandoandClaude Opus 5.5 3c6999d700 Spectators on a server see through the entity they spectate
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]>
2026-10-30 14:19:25 +07:00
d3grandoandClaude Opus 5.5 956bd64c5d Death loot parity: endermen carrying each holdable block
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]>
2026-10-30 14:11:35 +07:00
d3grandoandClaude Opus 5.5 7a1a5e8e88 Drop a killed enderman's carried block
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]>
2026-10-30 14:11:33 +07:00
d3grandoandClaude Opus 5.5 16356bdc57 Explosions break paintings, cushions and armour stands
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]>
2026-10-30 14:07:16 +07:00
d3grandoandClaude Opus 5.5 6be3dad37b lan_e2e: tag-query check for an operator guest's F3+I
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:05:51 +07:00
d3grandoandClaude Opus 5.5 ef024a082c F3+I copies the server's data on servers and LAN
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]>
2026-10-30 14:05:51 +07:00
d3grandoandClaude Opus 5.5 e9eda298fd Complete chat with the server's custom chat completions
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]>
2026-10-30 14:05:27 +07:00
d3grandoandClaude Opus 5.5 958522ce67 Let armadillos breed, follow spider eyes and their parents
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]>
2026-10-30 14:03:02 +07:00
d3grandoandClaude Opus 5.5 84ac601f3a Slimes and magma cubes hurt the iron golems they touch
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]>
2026-10-30 13:59:53 +07:00
d3grandoandClaude Opus 5.5 aff1d9864f The mount a rider's client moves ignores the server's moves of it
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]>
2026-10-30 13:59:23 +07:00
d3grandoandClaude Opus 5.5 699fae6296 Riders move their horses on servers, and jump them
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]>
2026-10-30 13:37:32 +07:00
d3grandoandClaude Opus 5.5 9bdb5ec7fd List worlds with an unreadable level.dat and offer recovery
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]>
2026-10-30 13:33:33 +07:00
d3grandoandClaude Opus 5.5 9432147489 A chat message the server deletes becomes the deleted marker
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]>
2026-10-30 13:30:38 +07:00
d3grandoandClaude Opus 5.5 d5fd62c298 Merge origin/main into fix-2755
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]>
2026-10-30 13:25:00 +07:00
d3grandoandClaude Opus 5.5 3891dd9410 Break sound and crumbs for equipment a server says broke
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]>
2026-10-30 13:22:55 +07:00
d3grandoandClaude Opus 5.5 1dd92e5f84 Drowned, zombie and stray conversions keep gear, name and flags
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]>
2026-10-30 13:20:25 +07:00
d3grandoandClaude Opus 5.5 350d5dbe2d Send and handle ClientboundChunksBiomesPacket so /fillbiome recolours loaded chunks
/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]>
2026-10-30 13:18:50 +07:00
d3grandoandClaude Opus 5.5 91aa9c790f Get off a strider onto the shore, not into the lava beside it
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]>
2026-10-30 13:17:31 +07:00
d3grandoandClaude Opus 5.5 3e536cb541 Merge origin/main into fix-3061-pad
game/pad.rs: main dropped the unused mouse::Rotation import; this branch's keymapping::SNEAK import stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:15:22 +07:00
d3grandoandClaude Opus 5.5 e445e0edf6 Spectating players are tracked only by other spectators
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]>
2026-10-30 13:08:40 +07:00
d3grandoandClaude Opus 5.5 4f03c8a873 Time out a server that stops answering, with vanilla's disconnect reasons
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]>
2026-10-30 13:08:10 +07:00
d3grandoandClaude Opus 5.5 9aeb985305 lan_e2e eating: one loaf each, and read the bites once they settle
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]>
2026-10-30 10:26:33 +07:00
d3grandoandClaude Opus 5.5 633cd1dbce Merge origin/main into fix-3393-optab
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]>
2026-10-30 08:55:23 +07:00
d3grandoandClaude Opus 5.5 03ba7662d5 Merge origin/main into fix-2740
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]>
2026-10-30 08:37:34 +07:00
d3grandoandClaude Opus 5.5 4d1f533af6 Creative tab selection test: give the category tab a stack
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]>
2026-10-30 06:52:26 +07:00
d3grandoandClaude Opus 5.5 89f40134b2 Merge origin/main into fix-3199
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]>
2026-10-30 06:50:11 +07:00
d3grandoandClaude Opus 5.5 f84b88b2e9 Thrown pearls feel the water their box touches, as vanilla's isInWater
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]>
2026-10-30 06:40:21 +07:00
d3grandoandClaude Opus 5.5 6b8e3b8f5f Runtime spawns run Mob.finalizeSpawn and wither skeletons hit for 4
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]>
2026-10-30 06:37:46 +07:00
d3grandoandClaude Opus 5.5 0b0a91e4d4 Mob AI parity: a wolf shaking dry and a wolf in the rain
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:37:22 +07:00
d3grandoandClaude Opus 5.5 5ee04257c5 Wet wolves darken and shake off the water on clients
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]>
2026-10-30 06:37:22 +07:00
d3grandoandClaude Opus 5.5 8c1f50f616 The Void superflat preset gets its stone start platform
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]>
2026-10-30 06:28:52 +07:00
d3grandoandClaude Opus 5.5 413e6e856e Swapping the same item into the off hand plays the re-equip animation
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]>
2026-10-30 06:24:02 +07:00
d3grandoandClaude Opus 5.5 5550a6c327 Read the glint clock and options once per frame, not per item
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]>
2026-10-30 06:06:42 +07:00
d3grandoandClaude Opus 5.5 70e83d9f65 Don't assert that a zombie pushes the ridden camel
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]>
2026-10-30 06:04:11 +07:00
d3grandoandClaude Opus 5.5 14d453048e Your body keeps up with the camera at Levitation 255
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]>
2026-10-30 06:03:07 +07:00
d3grandoandClaude Opus 5.5 d4a973f65f Creative inventory: the Operator Utilities tab for operators
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]>
2026-10-30 05:59:03 +07:00
d3grandoandClaude Opus 5.5 152258bb83 server_knockback harness: creative players near a crystal and TNT
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]>
2026-10-30 05:54:10 +07:00
d3grandoandClaude Opus 5.5 51cd5409da Creative players are pushed by explosions and crystals
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]>
2026-10-30 05:54:10 +07:00
d3grandoandClaude Opus 5.5 450ff69b4e Food grows a tamed baby cat or wolf instead of making it sit
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]>
2026-10-30 05:46:18 +07:00
d3grandoandClaude Opus 5.5 b3fdebcb92 Ender Dragon wings push the player only when their hit lands
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]>
2026-10-30 05:45:42 +07:00
d3grandoandClaude Opus 5.5 c744e51084 Keep same-vehicle mobs in the cramming count, and test the camel fix
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]>
2026-10-30 05:44:16 +07:00
d3grandoandClaude Opus 5.5 7f1edb9263 A camel and the husk on its back no longer push each other
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]>
2026-10-30 05:44:16 +07:00
d3grandoandClaude Opus 5.5 3ffaf67e22 Wolves tilt their head at food they want
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]>
2026-10-30 05:43:53 +07:00
d3grandoandClaude Opus 5.5 a8b5151ce5 Controller toggle-sneak comes back after a screen closes
The pad's toggle-sneak flipped sneaking with a hold-mode Shift press or
release, so KeyMapping.releaseAll dropped it when a screen opened and
restoreToggleStatesOnScreenClosed had nothing to restore. ToggleKeys now
has a pad toggle: it flips the sneak mapping as ToggleKeyMapping.setDown
does with its option on, and while it holds sneaking the mapping toggles
(released and remembered by a screen, restored when it closes, a shift
release behind the screen ignored), as vanilla's toggle crouch does.

Part of #3061.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:34:50 +07:00
d3grandoandClaude Opus 5.5 5b3b108515 A small player's server box follows minecraft:scale
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]>
2026-10-30 05:34:25 +07:00
d3grandoandClaude Opus 5.5 58cdd754cd No rain splashes or rain sound under a high roof
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]>
2026-10-30 05:30:02 +07:00
d3grandoandClaude Opus 5.5 dff6db1e86 Hide the own body when the server sets the player's invisible flag
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]>
2026-10-30 05:24:39 +07:00
d3grandoandClaude Opus 5.5 4b09c9d7d1 Thrown eggs and snowballs turn with the camera, no flip looking straight up
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]>
2026-10-30 05:18:25 +07:00
d3grandoandClaude Opus 5.5 5200d36a7b Withers build in any orientation, like vanilla's BlockPattern
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]>
2026-10-30 05:14:38 +07:00
d3grandoandClaude Opus 5.5 a18c9206d9 Falling sand and gravel ride bubble columns
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]>
2026-10-30 05:12:39 +07:00
d3grandoandClaude Opus 5.5 42ff9e6482 Players and heads keep their skins on busy servers
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]>
2026-10-30 05:01:48 +07:00
d3grandoandClaude Opus 5.5 b495d4931e Light a column again when an edit darkens it beside an unlit one
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]>
2026-10-30 04:59:05 +07:00
d3grandoandClaude Opus 5.5 0ce65d34c6 Both mouse buttons at once replace the block in creative
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]>
2026-10-30 04:58:07 +07:00
d3grandoandClaude Opus 5.5 f7b77fbc88 Many mobs no longer grow the renderer's staging memory until it runs out
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]>
2026-10-30 04:45:47 +07:00
d3grandoandClaude Opus 5.5 945ad05579 Darkness dims the screen on servers and in single player
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]>
2026-10-30 04:44:02 +07:00
d3grandoandClaude Opus 5.5 b80c1a9eef lan_e2e: cushions check covers the placing swing and changing seats
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:41:45 +07:00
d3grandoandClaude Opus 5.5 682f8a20fa Cushions swing the arm when placed and let a seated player change seats
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]>
2026-10-30 04:41:45 +07:00
d3grandoandClaude Opus 5.5 5aa2da4960 Applying a resource pack no longer freezes the window
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]>
2026-10-30 04:41:35 +07:00
d3grandoandClaude Opus 5.5 f5de793b38 Wither breaks parity: hold the wither in place until it breaks
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]>
2026-10-30 02:44:22 +07:00
d3grandoandClaude Opus 5.5 52a380cd2f Merge origin/main into fix-3503
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:30:27 +07:00
d3grandoandClaude Opus 5.5 150412d353 benchmarks: rerun the Windows RD 8 and RD 32 games on an idle machine
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]>
2026-10-30 02:30:03 +07:00
d3grandoandClaude Opus 5.5 9afb898e80 Merge remote-tracking branch 'origin/main' into fix-3153
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:15:41 +07:00
d3grandoandClaude Opus 5.5 ef3163c244 Merge origin/main into fix-3236
tools/lan_e2e.py: the mooshroom check and main's shulker_menu and keys_released checks, all kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:13:41 +07:00
d3grandoandClaude Opus 5.5 3931a46585 lan_e2e: the player beside the eater hears each bite once too
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]>
2026-10-30 02:07:35 +07:00
d3grandoandClaude Opus 5.5 34c77f9eac lan_e2e: check that a mooshroom's colour reaches the guest
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]>
2026-10-30 01:35:21 +07:00
d3grandoandClaude Opus 5.5 67a960b045 Merge origin/main into bench-windows-results
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:25:47 +07:00
d3grandoandClaude Opus 5.5 8168d96082 Merge origin/main into fix-3503
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]>
2026-10-30 01:12:36 +07:00
d3grandoandClaude Opus 5.5 e09cbf2b9e Merge origin/main into fix-3091
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]>
2026-10-30 01:07:41 +07:00
d3grandoandClaude Opus 5.5 63b8018505 Type AltGr characters into chat and search boxes on Windows
Windows reports AltGr as Ctrl plus Alt, and a layout's third-level
characters (ABNT2 `/` on Ctrl+Alt+Q, German `@ { [`, Polish letters)
arrive as text while a Control key is down. The chat box, the command
block's command, the world options' fields and the recipe and mod-panel
search boxes dropped any typed text while Control was held, so none of
these characters could be typed.

Vanilla's KeyboardHandler.textInput hands every code point to charTyped
whatever modifiers are held; EditBox.charTyped only checks
StringUtil.isAllowedChatCharacter. Text now reaches those fields unless
the edit modifier is held without Alt (where AltGr is Ctrl+Alt), so
Ctrl and Cmd shortcuts still type nothing.

Fixes #3001

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:46:23 +07:00
d3grandoandClaude Opus 5.5 158075a8e8 Eating: the eater hears each bite once
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]>
2026-10-29 18:41:12 +07:00
d3grandoandClaude Opus 5.5 e8c1a797c8 Mooshrooms keep their colour: save Type, sync DATA_TYPE at 18
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]>
2026-10-29 18:39:41 +07:00
d3grando 31cfdb4b95 Merge remote-tracking branch 'origin/main' into fix-3408
# Conflicts:
#	crates/horizon-client/src/game/render_tick.rs
2026-10-29 18:34:07 +07:00
d3grandoandClaude Opus 5.5 9fa7dddbe4 Let parrots fly: flying navigation and move control, flapping and gliding
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]>
2026-10-29 18:29:08 +07:00
d3grandoandClaude Opus 5.5 675298eac2 Screenshots on OpenGL draw the frame where it can be read back
OpenGL swapchain images offer RENDER_ATTACHMENT only, so the surface is
configured without COPY_SRC, but the screenshot path copied the swapchain
image into the readback buffer anyway: wgpu's validation aborted the game
("Usage flags RENDER_ATTACHMENT ... do not contain required usage flags
COPY_SRC") on every screenshot under the GL backend.

A frame that is read back on such a surface is now drawn into a texture
that can be copied from, read back from there, and then drawn onto the
swapchain image (wgpu::util::TextureBlitter), so the window still shows
it. Other frames, and surfaces with COPY_SRC, draw straight to the
swapchain as before. The check in write_screenshot that refused every
readback on these surfaces goes with it.

Fixes #3510

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:23:20 +07:00
d3grandoandClaude Opus 5.5 8b8ab7fd5d The mouse wheel selects a bundle's item in the creative inventory
The wheel over a bundle only reached bundle_scroll on the survival
inventory and container screens. On the creative screen it always
scrolled the item grid, and the hover tick that clears the selection
(onStopHovering) didn't run there either.

CreativeModeInventoryScreen.mouseScrolled calls
AbstractContainerScreen.mouseScrolled first, which offers the wheel to
the hovered slot's ItemSlotMouseActions (BundleMouseActions), and only
scrolls the grid when none took it. takes_bundle_wheel names the
screens that do this (inventory and container menus, and creative), and
both the wheel dispatch and tick_bundle_hover use it.

Fixes #3211

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:23:06 +07:00
d3grandoandClaude Opus 5.5 2f3f8d7a7f Draw lightning bolts: sample inside the white sprite
The bolt quads (RenderTypes.lightning has no texture) sampled the atlas at
the white sprite's corner (u0, v0). That point is on the sprite's border,
where the atlas's nearest sampler picks the neighbouring transparent texels
for most fragments, so a bolt came out as a few scattered pixels in
singleplayer and on servers alike. Sample the middle of the sprite, as the
clouds do.

Fixes #3171

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:22:54 +07:00
d3grandoandClaude Opus 5.5 17e0143745 Shader packs: a program the shader compiler panics on no longer crashes the game
Photon v1.4's gbuffers programs make naga's SPIR-V writer panic
("Expression [89] is not cached!") while wgpu-hal's Vulkan backend makes
the render pipeline in PackPost::new. Nothing caught it, so the game
crashed. The pack stays picked in horizon-graphics.txt, so every later
world load crashed again until the file was edited by hand (#3525).

- crash::contain runs a closure under catch_unwind and returns its
  panic as an error. The panic hook logs such a panic with its stack
  and makes no crash report.
- pack_post::checked, which builds every pack program's pipeline under
  a validation error scope, contains a panic too. The pass is skipped
  with a note, as a validation error already skips it. The rest of the
  pack runs, and the device keeps working.
- Any other panic while loading a pack (translation, PackPost::new)
  turns the pack off. Nothing runs in its place, it is unpicked and
  saved so the next start does not load it again, and the chat says so
  in red, as a refused Horizon pack already does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:11:14 +07:00
d3grandoandClaude Opus 5.5 8469dcc953 Graphics settings: the scrollbar can be held and dragged
On the Simple and Advanced graphics pages, the scrollbar was drawn but
GraphicsUi::click never checked for it. A press on it did nothing, and
a drag only moved a held slider, so the list could only be scrolled
with the wheel (#3552).

GraphicsUi now keeps its scroll in ui::Scroll, the AbstractScrollArea
port the other lists use:
- a press on the scrollbar column holds it, silently (updateScrolling);
- dragging moves the list with the pointer, and going above or below
  the list jumps to an end (mouseDragged);
- releasing lets go (onRelease).
The column is computed once, for both drawing and input.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:09:08 +07:00
d3grandoandClaude Opus 5.5 9f28fe8903 startup: report why resource loading failed instead of "loader stopped"
The loader's result was taken by App::about_to_wait to end loading, but the
event loop polls once more after exit(): the loader had finished, its channel
was closed, and poll_menu_resources replaced the real error with "resource
loader stopped before finishing". Every failed load (missing or stale prepared
files) ended with that message.

- Loading keeps its first error and stops polling once Done has arrived.
- A loader that really panicked reports its panic message.
- Before any window or loader, a missing horizon-resources.json names the
  folder it was looked for in and how to prepare it (the launcher, or
  horizon-prepare --prepare).

Fixes #3597

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:07:05 +07:00
d3grandoandClaude Opus 5.5 e086b2cfdd Wolves: any target makes a wolf angry, so an alerted pack looks angry
Wolf.aiStep ends with NeutralMob.updatePersistentAnger(level, true): a
wolf with a target is angry at it, and the 20-39 s timer
(PERSISTENT_ANGER_TIME) starts over every tick it holds one. Horizon only
set the anger in hurtServer when the player hit the wolf, so the pack
mates HurtByTargetGoal alerted attacked without the angry texture, and the
random draws of an angry wolf came in the wrong order. isAngryAt(player)
now reads the anger's target, and stopBeingAngry ends it when the time
runs out, the target mob dies, or the player is in creative, spectator
or peaceful. Saving a wolf keeps that anger target instead of replacing
it with the current target.

The mob_ai harness prints a wolf's anger and gets a wolf_hurt scenario.

Fixes #3185

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:58:42 +07:00
d3grandoandClaude Opus 5.5 daa5415724 Paintings: the item's variant is placed and named in its tooltip
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]>
2026-10-29 15:54:33 +07:00
d3grandoandClaude Opus 5.5 4bd3210f78 Commands: coordinate suggestions skip entities and offer the hit point
ClientSuggestionProvider.getRelevantCoordinates and getAbsoluteCoordinates
read Minecraft.hitResult only when it is a block hit. Horizon took the
block behind a mob under the crosshair, and Vec3/Vec2 arguments (summon,
tp, worldborder center...) only ever offered ~. Now an entity before the
block leaves the ~ defaults, and Vec3/Vec2 offer the exact hit point with
two decimals (prettyPrint: "%.2f", Locale.ROOT).

Fixes #3427

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:52:30 +07:00
d3grandoandClaude Opus 5.5 a30b9525d1 Horses rear when hurt
AbstractHorse.hurtServer had no port: after a hurt that went through,
vanilla draws random.nextInt(3) and on 0 calls standIfPossible, which
rears the horse for 20 ticks unless canPerformRearing is false (llamas,
camels). Mob::damage_with now ends with that roll for every equine, and
make_mad shares the new stand_if_possible.

The stand counter runs down in AbstractHorse.tick, with or without AI,
but Horizon ran it down only in the AI step, so a NoAI horse that reared
from a hit stayed reared for good. A mob with no AI now runs it down too.

The mob_ai harness gets a llama_hurt scenario (the draw without the
rear), which matched for 10 ticks before and all 201 after; the probe
names MountPanic and LlamaHurtBy as vanilla does.

Fixes #3432

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:45:42 +07:00
d3grandoandClaude Opus 5.5 ced6323bb2 Single player: save when the game pauses
IntegratedServer.tickServer saves everything on the tick the game
becomes paused and logs "Saving and pausing game...". Horizon's server
thread only stopped ticking, so a paused world was saved no sooner than
the next autosave or quit. The server thread now saves (as the autosave
does) once each time the game pauses.

agent_control_e2e gets pause-save: opening the pause menu writes
level.dat.

Fixes #3280

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:36:00 +07:00
d3grandoandClaude Opus 5.5 698e7fd662 Input: release held keys when leaving a server
Keys held when the player left a server stayed in Game::keys: once the
title or disconnect screen is open their releases go to that screen, so
W and Space held through a LAN host closing the world walked and jumped
on in the next world. Vanilla's Gui.setScreen calls KeyMapping.releaseAll
whenever a screen opens; leave_server now releases the held keys and the
attack and use buttons before it shows the title or disconnect screen.

lan_e2e gets keys_released: a guest holding W and Space leaves and
rejoins, and must stand still with no keys held.

Fixes #3246

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:35:58 +07:00
d3grandoandClaude Opus 5.5 cf0b5a8e63 Mobs: undead with NoAI burn in daylight
NoAI only skips Mob.serverAiStep; Mob.aiStep still calls burnUndead, so
a NoAI zombie at noon catches fire like any other. Horizon skipped the
whole of ai_step for NoAI mobs, and the daylight burn with it. The burn
is now its own burn_undead, run with or without AI.

The mob_ai harness gets zombie_noai_day_burn (a NoAI zombie in the sun).

Fixes #3430

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:35:53 +07:00
d3grandoandClaude Opus 5.5 7808916266 Ominous bottles list their Bad Omen level and duration in the tooltip
OminousBottleAmplifier.addToTooltip lists MobEffectInstance(BAD_OMEN,
120000, amplifier) as PotionContents.addPotionTooltip does: blue (NEUTRAL),
"Bad Omen IV (1:40:00)". Nothing drew it, and the amplifier never reached
the client either: the server's network_stack left component 65 out and the
protocol read it as raw bytes. It now goes over the wire as a VarInt
(OminousBottleAmplifier.STREAM_CODEC) and item_sync decodes it.

Fixes #3501

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:32:08 +07:00
d3grandoandClaude Opus 5.5 53beacfd13 Let the off hand go when a bucket, bottle or boat has no target
On a server client (single player included) remote_use_item predicted
SUCCESS for every bucket, glass bottle and boat, which ended
Minecraft.startUseItem before the off hand's turn: a Riptide trident
there never charged. BucketItem, BottleItem and BoatItem.use PASS or
FAIL with nothing to fill, empty or place, and startUseItem stops only
on SUCCESS. The client now checks the target first (a waterlogged block
counts as a water source, as SimpleWaterloggedBlock is a BucketPickup);
place_boat is split into boat_to_place (no side effects) so both share
BoatItem's checks. A powder snow bucket (SolidBucketItem, only useOn)
always passes its use.

Fixes #3147

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:31:50 +07:00
d3grandoandClaude Opus 5.5 11d3f08c12 Wither: broken blocks give way to their fluid, so water stays water
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]>
2026-10-29 15:28:03 +07:00
d3grandoandClaude Opus 5.5 6a59002283 Creative tooltips list the tabs that hold the item, as vanilla does
CreativeModeInventoryScreen.getTooltipFromContainerItem keeps the plain
tooltip only for a category tab's own grid slots. Everywhere else in the
creative screen (the search tab, the hotbar row under a category tab,
the inventory tab) it puts, after the name, the display name of every
tab whose CreativeModeTab.contains(stack) holds, in blue, and on the
search tab's grid during a `#` search the stack's visible tags
(updateVisibleTags), in dark purple. Horizon drew the plain tooltip
everywhere.

Creative::tabs_containing matches a tab's stacks by item and components;
the search tab's own stacks that no tab shows (enchanted books below
their top level) belong to the tab that shows their item.
Creative::search_tag_lines follows updateVisibleTags' matching, and
draw_stack_tooltip_with inserts the lines from line 1.

Fixes #3426

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:27:35 +07:00
d3grandoandClaude Opus 5.5 ab9ceb6a6d Wither: stay still while charging, and spare itself from its spawn blast
While a wither charges (Invul > 0), WitherBoss.customServerAiStep returns
before setAlternativeTarget(0, ...), so aiStep has no target to fly to.
Horizon stored the target's position first, so the charging wither flew
toward whatever it targeted. The position is now taken after the charge.

The blast that ends the charge is level.explode(this, ...): the wither is
the source and ServerExplosion's getEntities(source, box) leaves it out.
The wither now names itself as the explosion source (as a creeper does),
and the blast's mob loop skips a source named that way, so it is neither
hurt nor pushed. A source only guessed from the mobs near the centre (a
sulfur cube, already off the list) may be a bystander and is still hurt.

The command_exec corpus gets a "wither spawn" group: a wither summoned
with Invul:20 next to a pig, its position mid-charge and its health after
the blast.

Fixes #3322

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:25:46 +07:00
d3grandoandClaude Opus 5.5 46d466f7c3 An eye of ender stays in use while the button is held, slowing the player
EnderEyeItem.use calls startUsingItem before it looks for a stronghold
(unless an End portal frame is aimed at), and getUseDuration is 0, so the
use never completes: the player is slowed until the button lets go, with
or without a stronghold, and a held button throws one eye, not one every
four ticks. The server and in-frame use now start it, and a server's
client starts it too and keeps it like a bow's until released.

Fixes #3505

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:24:52 +07:00
d3grandoandClaude Opus 5.5 113cafd2d1 Copper golem eyes glow on the body model, not the empty eyes layer
CopperGolemRenderer's LivingEntityEmissiveLayer (RenderTypes.eyes) draws
the eye texture on a CopperGolemModel baked from ModelLayers.COPPER_GOLEM,
the body's layer. Horizon drew it on copper_golem#eyes, whose prepared model
has no polygons, so the eyes showed the body texture's black sockets.

Fixes #2862

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:24:31 +07:00