Vanilla's Font draws every literal through StringDecomposer.iterateFormatted:
a § code styles what follows and is not drawn, a lone trailing § is
dropped. The sidebar drew owner names, team affixes, scores and the title
raw, so servers' sidebars showed § and the code letters. They now go
through Text::resolve_legacy, as the tab list already does.
The HUD harness's sidebar cases gain legacy-coded owners, affixes, titles
and fixed score formats.
Fixes#937
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 Agent: merged after verify passed. Remote players' projectiles record their owner: ender pearls teleport and hurt the thrower, mobs target the shooter; docs/server.md row updated; part of #484.
🤖 Agent: merged after verify passed. A remote player's item used on a block is used once (a snowball thrown at the ground flew 2× main hand / 3× off hand); part of #484.
A projectile a guest shot was recorded as the local player's (Who::Player), which on a
server means the host, or nobody on a dedicated server. So a guest's ender pearl
teleported the host on a LAN world and did nothing on a dedicated server, what it hit
answered to the host, and clients were told the projectile had no owner.
Projectiles now remember the guest who shot them (Projectile.guest, set when the
guest's turn ends). Clients get the guest as the owner (Projectile.getAddEntityPacket),
and the hit runs as that guest: their pearl teleports and hurts them, and the mob hit
answers to them. ThrownEnderpearl's endermite also needs monsters to spawn, and comes
where the player was.
The dedicated-server harness checks that the snowballs' owner is the player and throws
an ender pearl at the player's feet: vanilla teleports it and takes 5 health, main did
neither.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A vanilla client sends a right click as one packet per hand and part
(Minecraft.startUseItem): UseItemOn for the block's part and ItemStack.useOn, then
UseItem for Item.use when that passed, the main hand first. The server ran the whole
click, both hands, for UseItemOn, then the item again for UseItem: a snowball thrown
at the ground flew twice, three times from the off hand.
Each packet now does its own part with its own hand
(ServerGamePacketListenerImpl.handleUseItemOn / handleUseItem): UseItemOn stops at
useOn (no throwing, drinking, eating, raising, equipping, buckets or an ender eye off a
frame), and neither touches entities (Interact does). Horizon's own client, which
counted on the server trying the off hand, now sends the off hand's packets when the
main hand's use passes, as vanilla's does. UseItem's rotation is the player's now
(absSnapRotationTo) without sending the client a teleport back.
The dedicated-server harness right-clicks the ground with a snowball in the main hand,
then in the off hand, and counts the snowballs against vanilla's (2 and 3 on main,
1 and 1 now).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Player attacks between the host and guests, or two guests, only checked the pvp game
rule. ServerPlayer.canHarmPlayer also asks the victim's team (Player.canHarmPlayer):
an ally on a team with friendlyFire false takes no damage, and the attacker hears the
no-damage sound.
lan_e2e's pvp check puts both players on such a team and checks that neither hurts
the other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dead guest's packets were all dropped but the respawn request, so a command typed on
the death screen never ran. ServerGamePacketListenerImpl.handleChatCommand and
handleSignedChatCommand don't look at the player's health: the commands now run.
The dedicated-server harness sends `/trigger nope` from the death screen and compares
the reply with vanilla's before respawning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 Agent: merged after verify passed. The site's nav links Host a server; README gains a hosting paragraph; tools/server.py and docs/server-quickstart.txt retired.
🤖 Agent: merged after verify passed. The host clears the hazards box, so a dead guest no longer leaves it behind for pvp/hurt (8/10 → 0/3 failures under --server-thread).
🤖 Agent: merged after verify and CI parity passed (command_exec 764/764 three times locally, 20 affected harnesses). /summon no longer spawns other pending entities and takes their id; the harness settle waits for entities like vanilla's. Fixes#947.
tools/server.py and docs/server-quickstart.txt are gone (horizon-server replaces them);
build_release.py builds only the client starter, and the docs point to docs/hosting.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The void can kill the guest before the hazards check tidies its stone box,
and the LAN server drops a dead guest's packets, so the box stayed: the
guest suffocated in it during pvp, and the hurt check's pig with it. The
host runs the tidying fills instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coming back to a level, the entities saved with its chunks wait in
World::loaded_entities until the next tick spawns them, as vanilla's
PersistentEntitySectionManager.processPendingLoads does. /summon pushed its
compound onto that queue and drained all of it, so the first waiting
entity took the id counted as the summon's root: after a Nether round trip
`/summon minecraft:armor_stand ...` reported "Summoned new Pig". Summon now
spawns only its own stack and leaves the queue to the tick.
The command harness's settle now waits for the entities of the chunks
around the player as well, like the oracle's settle
(areEntitiesActuallyLoadedAndTicking), and a `portal` op sends the player
through the nether portal it stands in (Entity.handlePortal:
NetherPortalBlock.getPortalDestination, teleportToPortalDestination). The
corpus's new `levels` group makes the round trip and checks the Overworld's
pig and a summon's feedback on the way back.
Fixes#947
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #880 added Source::level and Source::elsewhere for server players
teleported into another level; this branch adds Source::dimension and
moves the local player. Kept dimension (it covers what elsewhere was read
for) and level; /tp tries main's teleport_across for a server's players
first, then this branch's local-player level switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 Agent: reviewed; verify passes. Release dry run on this branch (run 39167763008): linux, macos, windows builds and publish all succeed; horizon-server-linux-x86_64.tar.gz (28 MB) built and import-checked (no libasound/libudev). Agent's proof: 880/879 client tests in both builds, dedicated_server/server_entities/lan on the server build, install → init → start → bot join on the ThinkPad without the libraries.
🤖 Agent: reviewed; verify passes. Equal-work server benchmark: fixed exploring speed, 300 s warm-up, chunk-work comparison per run. Horizon 1.8/4.7/11.7/23.8 ms per tick at 1/10/25/50 bots vs vanilla 11.3/34.3/50.5/stopped by its watchdog; single runs on the ThinkPad, caveats in the README.
🤖 Agent: reviewed; verify and parity pass. tools/vanilla_client.py runs on macOS (vsync off so a hidden window doesn't block the join); the owner's vanilla 26.3 client joined a Horizon server and stayed connected 4 min.
🤖 Agent: thanks rdbt! Verify passes. Checked locally merged with current main: fmt and clippy clean, cargo test -p horizon-client (885 passed), 14 harnesses including your two new oracles (player_sync, interaction_input, local_input, sprint_input, item_use, item_use_start, item_use_lifecycle, use_item_on, teleport_consume, lan, dedicated_server, server_player_combat, server_player_targets, server_entities), and lan_e2e pvp, hurt, eat, eating, clicks, chat, respawn in both modes: all pass. Fixes#903.
Without a local player (dedicated server, --server-thread) no block ticked:
tick_blocks returned early, so crops, fluids, redstone, pistons and pressure
plates stood still, and a LAN host ticked only around itself.
ticket_levels ports DistanceManager's levels: PLAYER_LOADING at 31 within the
view distance and PLAYER_SIMULATION at 31 - simulation distance for every
player in the level (the local player, LAN guests, a server's players), and
FORCED at 31. Random ticks run in simulation entity-ticking chunks that are
ticking chunks, scheduled ticks, block events and block entities in block
ticking ones, entities in entity ticking ones; the sculk blocks follow the
same rule. Saved ticks unpack when a chunk starts ticking
(startTickingChunk), not when it loads.
The hub keeps the tick-rate state current while nobody is in the game, so a
player joining a frozen server is told once (updateJoiningPlayer).
dedicated_server parity froze nothing while the client was sent its chunks:
saved fluid ticks there (a lava fall that never settles) ran before or after
the chunk was sent depending on each server's loading speed, and it failed on
main too. It now freezes the game until the client has its chunks.
lan_e2e gets a growth check: water flows and wheat grows next to a guest
1000 blocks from the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tool picks this machine's natives (macOS arm64 or Linux), adds -XstartOnFirstThread on macOS
and turns vsync off there: with it on, an unseen window (a locked session) blocks in
SDL_GL_SwapWindow before the client joins. docs/server.md records the join.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: the hub's player state keeps both main's attributes (#857) and
#887's living_flags; remote.rs and lan.rs debug JSON keep swinging and
using; the guests' model tick keeps both the swing update and the item
in use; lan_e2e.py checks unioned. remote_player_eating reads main's
Option<Consumable> (#799): any consumable with particles, its own use
time and should_emit, as the local player does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
horizon-client's cpal output and its controllers (gilrs, the DualSense's HID output) move behind the
audio and controllers features, on by default, so the game is unchanged. Built with
--no-default-features it links neither libasound.so.2 nor libudev.so.1. release.yml packages that
build as horizon-engine in horizon-server-linux-x86_64.tar.gz (xtask package-server, checked by
check_imports.py --server); install.sh installs it on Linux (and --archive installs a local one);
horizon-server already prefers horizon-engine. The Dockerfile uses it too, and CI lints it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the server thread, lan_e2e --check light failed its border step (#891):
the guest's border chunk still had block light 0 next to glowstone placed
in the chunk past it. The server was right; the check was not.
same_light() waits until the host's light equals the guest's, but on the
server thread the host's game is a client too, and both are dark until
the light update arrives. They agreed at once, same_light returned, and
the next line read the guest's light before the update came.
The check now waits up to 100 ticks for the guest's border light, then
compares the host's with it.
With temporary logging on the server thread: the glowstone lands in a
finished chunk that isn't lit (the ring past what the guest tracks), so
the edit queues a full light job for its neighbours. The border chunk's
job then relights sections 15 to 17, and Hub::broadcast_light_changes
sends them to both clients as a tracked border chunk, as
ChunkMap.isChunkOnTrackedBorder does. Nothing in the engine changes.
Fixes#891
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Block triggers counted only the host's own player and the minecarts, so a
guest stepping on a plate did nothing. The guests in the level, not
spectators, now trigger them through Entity.checkInsideBlocks and are
counted by the plates' entity queries. On the server thread no block
ticks run at all yet (#890).
lan_e2e.py plates: a stone plate under the guest, on both sides.
Part of #848
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 Agent: Merged. CI parity and verify passed, and on current main with this branch, horizon-client clippy is clean and all its tests pass (873 lib + integration). Consumables now finish as in vanilla 26.3.
BrewingStandBlock opens BrewingStandMenu for a guest: the host clicks it on
the menu engine over the stand's slots (one bottle per slot, ingredient and
fuel slots, quick moves by item) and sends its four data slots
(BrewingStandBlockEntity.dataAccess). The guest opens minecraft:brewing_stand
menus, maps their slots, predicts clicks on Kind::Brewing and shows the
times. Reported by Tydragon00.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neither side sent LivingEntity.DATA_LIVING_ENTITY_FLAGS for players, so
no one saw another player eat, draw a bow or raise a shield. The host now
sends the item in use and its hand for itself and every guest (always:
the trackers send only the entries that changed, and stopping is a change
back to 0), and draws its guests' bodies using their items. A guest's
client starts and stops the other players' item use from the flags and
drops their eating crumbs at their mouth (Consumable.onConsumeTick); the
eating sounds already come from the server.
lan_e2e.py eating: the host and the guest each see the other eat bread,
and stop.
Part of #803
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host ignored ServerboundPunchPacket, so a guest mining or hitting
swung only on its own screen; only the first swing a guest's use ever
started went out, the host's own swings never did, the host drew no swing
on guests' bodies and a guest's client ignored
ClientboundSwingAnimationPacket.
The host now swings the guest's hand on a punch (handlePunch), sends each
tick's guest swings to the players around and the host's own to the
guests, and swings and ticks the guests' bodies it draws. A use the server
ran that swung goes back to the guest too: Horizon's client does not run
uses itself, so it never predicted that swing (the hoe). The guest's
client swings itself or the other entity on the packet.
lan_e2e.py swings: mining swings seen from each side, and the guest's own
hoe swing.
Part of #848
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host told guests of a hurt mob with EntityEvent 2, which 26.3's
clients read as a kinetic hit, not a hurt. It now sends
ClientboundDamageEventPacket (ServerLevel.broadcastDamageEvent) with the
damage type and the attacker, and the guest's client restarts the hurt
flash and jolts the walk animation (LivingEntity.handleDamageEvent).
lan_e2e.py hurt: a pig the guest hits and one the host hits flash on
both sides.
Part of #848
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>