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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
/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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
OpenGL swapchain images offer RENDER_ATTACHMENT only, so the surface is
configured without COPY_SRC, but the screenshot path copied the swapchain
image into the readback buffer anyway: wgpu's validation aborted the game
("Usage flags RENDER_ATTACHMENT ... do not contain required usage flags
COPY_SRC") on every screenshot under the GL backend.
A frame that is read back on such a surface is now drawn into a texture
that can be copied from, read back from there, and then drawn onto the
swapchain image (wgpu::util::TextureBlitter), so the window still shows
it. Other frames, and surfaces with COPY_SRC, draw straight to the
swapchain as before. The check in write_screenshot that refused every
readback on these surfaces goes with it.
Fixes#3510
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The wheel over a bundle only reached bundle_scroll on the survival
inventory and container screens. On the creative screen it always
scrolled the item grid, and the hover tick that clears the selection
(onStopHovering) didn't run there either.
CreativeModeInventoryScreen.mouseScrolled calls
AbstractContainerScreen.mouseScrolled first, which offers the wheel to
the hovered slot's ItemSlotMouseActions (BundleMouseActions), and only
scrolls the grid when none took it. takes_bundle_wheel names the
screens that do this (inventory and container menus, and creative), and
both the wheel dispatch and tick_bundle_hover use it.
Fixes#3211
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bolt quads (RenderTypes.lightning has no texture) sampled the atlas at
the white sprite's corner (u0, v0). That point is on the sprite's border,
where the atlas's nearest sampler picks the neighbouring transparent texels
for most fragments, so a bolt came out as a few scattered pixels in
singleplayer and on servers alike. Sample the middle of the sprite, as the
clouds do.
Fixes#3171
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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]>
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]>
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]>
ClientSuggestionProvider.getRelevantCoordinates and getAbsoluteCoordinates
read Minecraft.hitResult only when it is a block hit. Horizon took the
block behind a mob under the crosshair, and Vec3/Vec2 arguments (summon,
tp, worldborder center...) only ever offered ~. Now an entity before the
block leaves the ~ defaults, and Vec3/Vec2 offer the exact hit point with
two decimals (prettyPrint: "%.2f", Locale.ROOT).
Fixes#3427
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractHorse.hurtServer had no port: after a hurt that went through,
vanilla draws random.nextInt(3) and on 0 calls standIfPossible, which
rears the horse for 20 ticks unless canPerformRearing is false (llamas,
camels). Mob::damage_with now ends with that roll for every equine, and
make_mad shares the new stand_if_possible.
The stand counter runs down in AbstractHorse.tick, with or without AI,
but Horizon ran it down only in the AI step, so a NoAI horse that reared
from a hit stayed reared for good. A mob with no AI now runs it down too.
The mob_ai harness gets a llama_hurt scenario (the draw without the
rear), which matched for 10 ticks before and all 201 after; the probe
names MountPanic and LlamaHurtBy as vanilla does.
Fixes#3432
Co-Authored-By: Claude Opus 5.5 <[email protected]>
IntegratedServer.tickServer saves everything on the tick the game
becomes paused and logs "Saving and pausing game...". Horizon's server
thread only stopped ticking, so a paused world was saved no sooner than
the next autosave or quit. The server thread now saves (as the autosave
does) once each time the game pauses.
agent_control_e2e gets pause-save: opening the pause menu writes
level.dat.
Fixes#3280
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keys held when the player left a server stayed in Game::keys: once the
title or disconnect screen is open their releases go to that screen, so
W and Space held through a LAN host closing the world walked and jumped
on in the next world. Vanilla's Gui.setScreen calls KeyMapping.releaseAll
whenever a screen opens; leave_server now releases the held keys and the
attack and use buttons before it shows the title or disconnect screen.
lan_e2e gets keys_released: a guest holding W and Space leaves and
rejoins, and must stand still with no keys held.
Fixes#3246
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NoAI only skips Mob.serverAiStep; Mob.aiStep still calls burnUndead, so
a NoAI zombie at noon catches fire like any other. Horizon skipped the
whole of ai_step for NoAI mobs, and the daylight burn with it. The burn
is now its own burn_undead, run with or without AI.
The mob_ai harness gets zombie_noai_day_burn (a NoAI zombie in the sun).
Fixes#3430
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OminousBottleAmplifier.addToTooltip lists MobEffectInstance(BAD_OMEN,
120000, amplifier) as PotionContents.addPotionTooltip does: blue (NEUTRAL),
"Bad Omen IV (1:40:00)". Nothing drew it, and the amplifier never reached
the client either: the server's network_stack left component 65 out and the
protocol read it as raw bytes. It now goes over the wire as a VarInt
(OminousBottleAmplifier.STREAM_CODEC) and item_sync decodes it.
Fixes#3501
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server client (single player included) remote_use_item predicted
SUCCESS for every bucket, glass bottle and boat, which ended
Minecraft.startUseItem before the off hand's turn: a Riptide trident
there never charged. BucketItem, BottleItem and BoatItem.use PASS or
FAIL with nothing to fill, empty or place, and startUseItem stops only
on SUCCESS. The client now checks the target first (a waterlogged block
counts as a water source, as SimpleWaterloggedBlock is a BucketPickup);
place_boat is split into boat_to_place (no side effects) so both share
BoatItem's checks. A powder snow bucket (SolidBucketItem, only useOn)
always passes its use.
Fixes#3147
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A wither's block breaking (WitherBoss.customServerAiStep) calls
Level.destroyBlock, which sets the cell to fluidState.createLegacyBlock()
with flag 3. Horizon's mob BreakBlock event wrote air instead, without
neighbour updates: a wither in water carved an air pocket, a waterlogged
block lost its water, and the pool did not flow back in. The event now
sets the fluid's block through setBlock (skipped when that is already
the cell), and the wither's break sound (level event 1022) only counts
cells that changed, as destroyBlock's return value does.
Checked by a new "wither breaks" group in the command execution harness.
Fixes#3153
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreativeModeInventoryScreen.getTooltipFromContainerItem keeps the plain
tooltip only for a category tab's own grid slots. Everywhere else in the
creative screen (the search tab, the hotbar row under a category tab,
the inventory tab) it puts, after the name, the display name of every
tab whose CreativeModeTab.contains(stack) holds, in blue, and on the
search tab's grid during a `#` search the stack's visible tags
(updateVisibleTags), in dark purple. Horizon drew the plain tooltip
everywhere.
Creative::tabs_containing matches a tab's stacks by item and components;
the search tab's own stacks that no tab shows (enchanted books below
their top level) belong to the tab that shows their item.
Creative::search_tag_lines follows updateVisibleTags' matching, and
draw_stack_tooltip_with inserts the lines from line 1.
Fixes#3426
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While a wither charges (Invul > 0), WitherBoss.customServerAiStep returns
before setAlternativeTarget(0, ...), so aiStep has no target to fly to.
Horizon stored the target's position first, so the charging wither flew
toward whatever it targeted. The position is now taken after the charge.
The blast that ends the charge is level.explode(this, ...): the wither is
the source and ServerExplosion's getEntities(source, box) leaves it out.
The wither now names itself as the explosion source (as a creeper does),
and the blast's mob loop skips a source named that way, so it is neither
hurt nor pushed. A source only guessed from the mobs near the centre (a
sulfur cube, already off the list) may be a bystander and is still hurt.
The command_exec corpus gets a "wither spawn" group: a wither summoned
with Invul:20 next to a pig, its position mid-charge and its health after
the blast.
Fixes#3322
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderEyeItem.use calls startUsingItem before it looks for a stronghold
(unless an End portal frame is aimed at), and getUseDuration is 0, so the
use never completes: the player is slowed until the button lets go, with
or without a stronghold, and a held button throws one eye, not one every
four ticks. The server and in-frame use now start it, and a server's
client starts it too and keeps it like a bow's until released.
Fixes#3505
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CopperGolemRenderer's LivingEntityEmissiveLayer (RenderTypes.eyes) draws
the eye texture on a CopperGolemModel baked from ModelLayers.COPPER_GOLEM,
the body's layer. Horizon drew it on copper_golem#eyes, whose prepared model
has no polygons, so the eyes showed the body texture's black sockets.
Fixes#2862
Co-Authored-By: Claude Opus 5.5 <[email protected]>